From d57874257e92b52009810eafe3a2025091b65c98 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Thu, 27 Jul 2023 13:34:32 +0300 Subject: [PATCH] Add subscribe request (#159) feat(ee): add subscribe request Add the ability to construct and exec subscribe request with the following response parsing. feat: add emit messages / status effects feat: add event listeners Listeners implemented in form of a subscription object which allows polling on updates. test(contract-test): completed contract testing for subscribe Completed set of contract tests for subscription event engine. refactor(clippy): apply clippy suggestions fix: fix formatting warning --------- Co-authored-by: Xavrax --- .github/workflows/run-tests.yml | 2 + .github/workflows/run-validations.yml | 16 +- .gitignore | 7 + Cargo.toml | 35 +- examples/subscribe.rs | 82 ++ src/core/cryptor.rs | 25 +- src/core/deserialize.rs | 26 + src/core/deserializer.rs | 12 +- src/core/error.rs | 72 +- src/core/error_response.rs | 3 +- src/core/event_engine/effect.rs | 11 +- src/core/event_engine/effect_dispatcher.rs | 202 ++-- src/core/event_engine/effect_handler.rs | 4 +- src/core/event_engine/mod.rs | 149 ++- src/core/mod.rs | 25 +- src/core/retry_policy.rs | 279 ++++-- src/core/runtime.rs | 47 + src/core/types.rs | 139 +++ src/core/utils/encoding.rs | 72 ++ src/core/{ => utils}/headers.rs | 0 src/core/{ => utils}/metadata.rs | 0 src/core/utils/mod.rs | 6 + src/dx/access/builders/grant_token.rs | 75 +- src/dx/access/builders/revoke.rs | 79 +- src/dx/access/mod.rs | 2 +- src/dx/access/payloads.rs | 2 +- src/dx/access/result.rs | 2 +- src/dx/parse_token.rs | 16 +- src/dx/publish/builders.rs | 11 +- src/dx/publish/mod.rs | 115 ++- src/dx/publish/result.rs | 34 +- src/dx/pubnub_client.rs | 109 ++- src/dx/subscribe/builders/mod.rs | 32 + src/dx/subscribe/builders/subscribe.rs | 282 ++++++ src/dx/subscribe/builders/subscription.rs | 561 +++++++++++ src/dx/subscribe/cancel.rs | 56 ++ .../subscribe/event_engine/effect_handler.rs | 122 ++- .../event_engine/effects/emit_messages.rs | 81 +- .../event_engine/effects/emit_status.rs | 57 +- .../event_engine/effects/handshake.rs | 136 ++- .../effects/handshake_reconnection.rs | 150 ++- src/dx/subscribe/event_engine/effects/mod.rs | 263 ++++-- .../subscribe/event_engine/effects/receive.rs | 158 ++-- .../effects/receive_reconnection.rs | 188 ++-- src/dx/subscribe/event_engine/event.rs | 13 +- src/dx/subscribe/event_engine/invocation.rs | 53 +- src/dx/subscribe/event_engine/mod.rs | 63 +- src/dx/subscribe/event_engine/state.rs | 411 ++++---- src/dx/subscribe/mod.rs | 517 ++++++++++- src/dx/subscribe/result.rs | 607 ++++++++++++ src/dx/subscribe/subscription.rs | 47 - .../subscribe/subscription_configuration.rs | 63 ++ src/dx/subscribe/subscription_manager.rs | 253 +++++ src/dx/subscribe/types.rs | 878 +++++++++++++++++- src/lib.rs | 35 - src/providers/crypto_aescbc.rs | 54 +- src/providers/deserialization_serde.rs | 22 +- src/providers/futures_tokio.rs | 27 + src/providers/mod.rs | 3 + src/transport/middleware.rs | 10 +- src/transport/reqwest.rs | 54 +- tests/common/common_steps.rs | 48 +- tests/contract_test.rs | 50 +- tests/subscribe/mod.rs | 1 + tests/subscribe/subscribe_steps.rs | 175 ++++ 65 files changed, 5966 insertions(+), 1163 deletions(-) create mode 100644 examples/subscribe.rs create mode 100644 src/core/deserialize.rs create mode 100644 src/core/runtime.rs create mode 100644 src/core/types.rs create mode 100644 src/core/utils/encoding.rs rename src/core/{ => utils}/headers.rs (100%) rename src/core/{ => utils}/metadata.rs (100%) create mode 100644 src/core/utils/mod.rs create mode 100644 src/dx/subscribe/builders/mod.rs create mode 100644 src/dx/subscribe/builders/subscribe.rs create mode 100644 src/dx/subscribe/builders/subscription.rs create mode 100644 src/dx/subscribe/cancel.rs create mode 100644 src/dx/subscribe/result.rs delete mode 100644 src/dx/subscribe/subscription.rs create mode 100644 src/dx/subscribe/subscription_configuration.rs create mode 100644 src/dx/subscribe/subscription_manager.rs create mode 100644 src/providers/futures_tokio.rs create mode 100644 tests/subscribe/mod.rs create mode 100644 tests/subscribe/subscribe_steps.rs diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 69d78ffc..c167d1ba 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -51,6 +51,8 @@ jobs: token: ${{ secrets.GH_TOKEN }} features-path: tests/features - name: Run acceptance tests + env: + RUST_LOG: debug run: | cargo test --features contract_test --test contract_test - name: Expose acceptance tests reports diff --git a/.github/workflows/run-validations.yml b/.github/workflows/run-validations.yml index be3c2c20..2fd4581c 100644 --- a/.github/workflows/run-validations.yml +++ b/.github/workflows/run-validations.yml @@ -46,27 +46,25 @@ jobs: - name: Run cargo check tool to check if the code are valid run: | - cargo check --workspace --all-targets --features="full" + cargo check --workspace --all-targets --features="full" - name: Run cargo check tool to check if the raw domain code are valid run: | - cargo check --workspace --no-default-features --features="pubnub_only" + cargo check --workspace --no-default-features --features="pubnub_only" - name: Run cargo check tool to check if the `no_std` code are valid run: | - cargo check --workspace --all-targets --no-default-features --features="full_no_std" - + cargo check --workspace --all-targets --no-default-features --features="full_no_std" - name: Run cargo clippy tool to check if all the best code practices are followed run: | - cargo clippy --workspace --all-targets --features="full" -- -D warnings + cargo clippy --workspace --all-targets --features="full" -- -D warnings - name: Run cargo clippy tool to check if all the best code practices are followed for raw domain code run: | - cargo clippy --workspace --no-default-features --features="pubnub_only" -- -D warnings + cargo clippy --workspace --no-default-features --features="pubnub_only" -- -D warnings - name: Run cargo clippy tool to check if all the best code practices are followed for `no_std` code run: | - cargo clippy --workspace --all-targets --no-default-features --features="full_no_std" -- -D warnings - + cargo clippy --workspace --all-targets --no-default-features --features="full_no_std" -- -D warnings - name: Run cargo fmt tool to check if code are well formatted run: | - cargo fmt --check --verbose --all + cargo fmt --check --verbose --all cargo-deny: name: Check Cargo crate dependencies diff --git a/.gitignore b/.gitignore index 08149528..db225c16 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,14 @@ target Cargo.lock tests/features tests/reports +tests/logs # GitHub Actions # ################## .github/.release + +# IDE +.idea + +# OS +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index accc2025..bbc49e3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ build = "build.rs" [features] # Enables all non-conflicting features -full = ["publish", "access", "serde", "reqwest", "aescbc", "parse_token", "blocking", "std"] +full = ["publish", "access", "serde", "reqwest", "aescbc", "parse_token", "blocking", "std", "tokio"] # Enables all default features -default = ["publish", "serde", "reqwest", "aescbc", "std", "blocking"] +default = ["publish", "subscribe", "serde", "reqwest", "aescbc", "std", "blocking", "tokio"] # [PubNub features] @@ -41,6 +41,9 @@ serde = ["dep:serde", "dep:serde_json", "hashbrown/serde"] ## Enables reqwest implementation for transport layer reqwest = ["dep:reqwest", "dep:bytes"] +## Enables tokio runtime for subscribe loop +tokio = ["dep:tokio"] + ## Enables blocking implementation for transport layer blocking = ["reqwest?/blocking"] @@ -61,23 +64,25 @@ extra_platforms = ["spin/portable_atomic", "dep:portable-atomic"] # [Internal features] (not intended for use outside of the library) contract_test = ["parse_token", "publish", "access"] -full_no_std = ["serde", "reqwest", "aescbc", "parse_token", "blocking", "publish", "access", "subscribe"] +full_no_std = ["serde", "reqwest", "aescbc", "parse_token", "blocking", "publish", "access", "subscribe", "tokio"] full_no_std_platform_independent = ["serde", "aescbc", "parse_token", "blocking", "publish", "access", "subscribe"] pubnub_only = ["aescbc", "parse_token", "blocking", "publish", "access", "subscribe"] mock_getrandom = ["getrandom/custom"] event_engine = [] # TODO: temporary treated as internal until we officially release it -subscribe = ["event_engine"] +futures = ["dep:futures"] +futures_tokio = ["dep:tokio"] +subscribe = ["event_engine", "futures", "futures_tokio", "dep:async-channel"] [dependencies] async-trait = "0.1" log = "0.4" -hashbrown = "0.13" +hashbrown = "0.14.0" spin = "0.9" phantom-type = { version = "0.4.2", default-features = false } percent-encoding = { version = "2.1", default-features = false } base64 = { version = "0.21", features = ["alloc"], default-features = false } -derive_builder = {version = "0.12", default-features = false } +derive_builder = { version = "0.12", default-features = false } uuid = { version = "1.3", features = ["v4"], default-features = false } snafu = { version = "0.7", features = ["rust_1_46"], default-features = false } rand = { version = "0.8.5", default-features = false } @@ -93,7 +98,7 @@ serde_json = { version = "1.0", optional = true, features = ["alloc"] ,default-f # reqwest reqwest = { version = "0.11", optional = true } -bytes = {version = "1.4", default-features = false, optional = true } +bytes = { version = "1.4", default-features = false, optional = true } # crypto aes = { version = "0.8.2", optional = true } @@ -103,6 +108,11 @@ getrandom = { version = "0.2", optional = true } # parse_token ciborium = { version = "0.2.1", default-features = false, optional = true } +# subscribe +tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros", "time"] } +futures = { version = "0.3.28", optional = true } +async-channel = { version = "1.8", optional = true } + # extra_platforms portable-atomic = { version = "1.3", optional = true, default-features = false, features = ["require-cas", "critical-section"] } @@ -111,14 +121,13 @@ getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] async-trait = "0.1" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } wiremock = "0.5" env_logger = "0.10" -cucumber = { version = "0.19", features = ["output-junit"] } -futures = "0.3" +cucumber = { version = "0.20.0", features = ["output-junit"] } reqwest = { version = "0.11", features = ["json"] } test-case = "3.0" -hashbrown = { version = "0.13", features = ["serde"] } +hashbrown = { version = "0.14.0", features = ["serde"] } getrandom = { version = "0.2", features = ["custom"] } [build-dependencies] @@ -152,3 +161,7 @@ required-features = ["default", "blocking", "access"] name = "custom_origin" required-features = ["default"] +[[example]] +name = "subscribe" +required-features = ["default", "subscribe"] + diff --git a/examples/subscribe.rs b/examples/subscribe.rs new file mode 100644 index 00000000..330790eb --- /dev/null +++ b/examples/subscribe.rs @@ -0,0 +1,82 @@ +use futures::StreamExt; +use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; +use pubnub::{Keyset, PubNubClientBuilder}; +use serde::Deserialize; +use std::env; + +#[derive(Debug, Deserialize)] +struct Message { + // Allowing dead code because we don't use these fields + // in this example. + #[allow(dead_code)] + url: String, + #[allow(dead_code)] + description: 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 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!("running!"); + + let subscription = client + .subscribe() + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .heartbeat(10) + .filter_expression("some_filter") + .execute()?; + + 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::MessageAction(action) => { + println!("message action: {:?}", action) + } + Update::File(file) => { + println!("file: {:?}", file) + } + } + } + SubscribeStreamEvent::Status(status) => println!("\nstatus: {:?}", status), + } + })); + + // Sleep for a minute. Now you can send messages to the channels + // "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; + + // You can also cancel the subscription at any time. + subscription.unsubscribe().await; + + Ok(()) +} diff --git a/src/core/cryptor.rs b/src/core/cryptor.rs index acbcbb58..b42ef131 100644 --- a/src/core/cryptor.rs +++ b/src/core/cryptor.rs @@ -4,7 +4,7 @@ //! encryption and decryption of published data. use crate::core::error::PubNubError; -use crate::lib::alloc::vec::Vec; +use crate::lib::{alloc::vec::Vec, core::fmt::Debug}; /// This trait is used to encrypt and decrypt messages sent to the /// [`PubNub API`]. @@ -24,24 +24,17 @@ use crate::lib::alloc::vec::Vec; /// ``` /// use pubnub::core::{Cryptor, error::PubNubError}; /// +/// #[derive(Debug)] /// struct MyCryptor; /// /// impl Cryptor for MyCryptor { -/// fn encrypt<'en, T>(&self, source: T) -> Result, PubNubError> -/// where -/// T: Into<&'en [u8]> -/// { +/// fn encrypt(&self, source: Vec) -> Result, PubNubError> { /// // Encrypt provided data here -/// /// Ok(vec![]) /// } /// -/// fn decrypt<'de, T>(&self, source: T) -> Result, PubNubError> -/// where -/// T: Into<&'de [u8]> -/// { +/// fn decrypt(&self, source: Vec) -> Result, PubNubError> { /// // Decrypt provided data here -/// /// Ok(vec![]) /// } /// } @@ -49,22 +42,18 @@ use crate::lib::alloc::vec::Vec; /// /// [`dx`]: ../dx/index.html /// [`PubNub API`]: https://www.pubnub.com/docs -pub trait Cryptor { +pub trait Cryptor: Debug + Send + Sync { /// Decrypt provided data. /// /// # Errors /// Should return an [`PubNubError::Encryption`] if provided data can't /// be encrypted or underlying cryptor misconfigured. - fn encrypt<'en, T>(&self, source: T) -> Result, PubNubError> - where - T: Into<&'en [u8]>; + fn encrypt(&self, source: Vec) -> Result, PubNubError>; /// Decrypt provided data. /// /// # Errors /// Should return an [`PubNubError::Decryption`] if provided data can't /// be decrypted or underlying cryptor misconfigured. - fn decrypt<'de, T>(&self, source: T) -> Result, PubNubError> - where - T: Into<&'de [u8]>; + fn decrypt(&self, source: Vec) -> Result, PubNubError>; } diff --git a/src/core/deserialize.rs b/src/core/deserialize.rs new file mode 100644 index 00000000..3affb1c6 --- /dev/null +++ b/src/core/deserialize.rs @@ -0,0 +1,26 @@ +//! Deserialization module +//! +//! This module provides a [`Deserialize`] trait for the Pubnub protocol. +//! +//! You can implement this trait for your own types, or use one of the provided +//! features to use a deserialization library. +//! +//! [`Deserialize`]: trait.Deserialize.html + +use crate::core::PubNubError; + +/// Deserialize values +/// +/// This trait provides a [`deserialize`] method for the Pubnub protocol. +/// +/// You can implement this trait for your own types, or use the provided +/// implementations for [`Into>`]. +/// +/// [`deserialize`]: #tymethod.deserialize +pub trait Deserialize<'de>: Send + Sync { + /// Type to which binary data should be mapped. + type Type; + + /// Deserialize the value + fn deserialize(bytes: &'de [u8]) -> Result; +} diff --git a/src/core/deserializer.rs b/src/core/deserializer.rs index bce16806..2b3f29ef 100644 --- a/src/core/deserializer.rs +++ b/src/core/deserializer.rs @@ -3,7 +3,7 @@ //! This module contains the `Deserialize` trait which is used to implement //! deserialization of Rust data structures. -use super::PubNubError; +use crate::core::PubNubError; /// Trait for deserializing Rust data structures. /// @@ -30,8 +30,8 @@ use super::PubNubError; /// /// struct MyDeserializer; /// -/// impl<'de> Deserializer<'de, PublishResult> for MyDeserializer { -/// fn deserialize(&self, bytes: &'de [u8]) -> Result { +/// impl Deserializer for MyDeserializer { +/// fn deserialize(&self, bytes: &[u8]) -> Result { /// // ... /// # unimplemented!() /// } @@ -42,8 +42,8 @@ use super::PubNubError; /// [`PublishResponseBody`]: ../../dx/publish/result/enum.PublishResponseBody.html /// [`GrantTokenResponseBody`]: ../../dx/access/result/enum.GrantTokenResponseBody.html /// [`RevokeTokenResponseBody`]: ../../dx/access/result/enum.RevokeTokenResponseBody.html -pub trait Deserializer<'de, T> { - /// Deserialize a `&[u8]` into a `Result`. +pub trait Deserializer: Send + Sync { + /// Deserialize a `&Vec` into a `Result`. /// /// # Errors /// @@ -51,5 +51,5 @@ pub trait Deserializer<'de, T> { /// deserialization fails. /// /// [`PubNubError::DeserializationError`]: ../enum.PubNubError.html#variant.DeserializationError - fn deserialize(&self, bytes: &'de [u8]) -> Result; + fn deserialize(&self, bytes: &[u8]) -> Result; } diff --git a/src/core/error.rs b/src/core/error.rs index e29c9e08..587af0c2 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -4,7 +4,10 @@ //! //! [`pubnub`]: ../index.html -use crate::lib::{alloc::string::String, alloc::vec::Vec}; +use crate::{ + core::TransportResponse, + lib::alloc::{boxed::Box, string::String, vec::Vec}, +}; use snafu::Snafu; /// PubNub error type @@ -35,6 +38,9 @@ pub enum PubNubError { Transport { ///docs details: String, + + /// Failed request HTTP status code. + response: Option>, }, /// this error is returned when the publication of the request fails @@ -101,6 +107,17 @@ pub enum PubNubError { details: String, }, + /// this error is returned when the event engine effect is canceled + #[snafu(display("Event engine effect has been canceled"))] + EffectCanceled, + + /// this error is returned when the subscription initialization fails + #[snafu(display("Subscription initialization error: {details}"))] + SubscribeInitialization { + ///docs + details: String, + }, + ///this error is returned when REST API request can't be handled by service. #[snafu(display("REST API error: {message}"))] API { @@ -118,6 +135,9 @@ pub enum PubNubError { /// List of channel groups which is affected by error. affected_channel_groups: Option>, + + /// Raw service response. + response: Option>, }, } @@ -126,8 +146,12 @@ impl PubNubError { /// /// This function used to inform about not initialized request parameters or /// validation failure. - #[cfg(any(feature = "publish", feature = "access"))] - pub(crate) fn general_api_error(message: S, status: Option) -> Self + #[cfg(any(feature = "publish", feature = "access", feature = "subscribe"))] + pub(crate) fn general_api_error( + message: S, + status: Option, + response: Option>, + ) -> Self where S: Into, { @@ -137,6 +161,48 @@ impl PubNubError { service: None, affected_channels: None, affected_channel_groups: None, + response, + } + } + + /// Retrieve attached service response. + #[cfg(any(feature = "publish", feature = "access", feature = "subscribe"))] + pub(crate) fn transport_response(&self) -> Option> { + match self { + PubNubError::API { response, .. } | PubNubError::Transport { response, .. } => { + response.clone() + } + _ => None, + } + } + + /// Attach service response. + /// + /// For better understanding some errors may provide additional information + /// right from service response. + #[cfg(any(feature = "publish", feature = "access", feature = "subscribe"))] + pub(crate) fn attach_response(self, service_response: TransportResponse) -> Self { + match &self { + PubNubError::API { + status, + message, + service, + affected_channels, + affected_channel_groups, + .. + } => PubNubError::API { + status: *status, + message: message.clone(), + service: service.clone(), + affected_channels: affected_channels.clone(), + affected_channel_groups: affected_channel_groups.clone(), + response: Some(Box::new(service_response)), + }, + PubNubError::Transport { details, .. } => PubNubError::Transport { + details: details.clone(), + response: Some(Box::new(service_response)), + }, + _ => self, } } } diff --git a/src/core/error_response.rs b/src/core/error_response.rs index 39374d5d..bfd6b546 100644 --- a/src/core/error_response.rs +++ b/src/core/error_response.rs @@ -11,7 +11,7 @@ use crate::lib::{ string::{String, ToString}, vec::Vec, }, - collections::hash_map::HashMap, + collections::HashMap, }; /// Implementation for [`APIError`] to create struct from service error response @@ -24,6 +24,7 @@ impl From for PubNubError { service: value.service(), affected_channels: value.affected_channels(), affected_channel_groups: value.affected_channel_groups(), + response: None, } } } diff --git a/src/core/event_engine/effect.rs b/src/core/event_engine/effect.rs index 97ebe5c7..cc280143 100644 --- a/src/core/event_engine/effect.rs +++ b/src/core/event_engine/effect.rs @@ -1,18 +1,17 @@ use crate::{ core::event_engine::EffectInvocation, - lib::alloc::{string::String, vec::Vec}, + lib::alloc::{boxed::Box, string::String, vec::Vec}, }; -pub(crate) trait Effect { +#[async_trait::async_trait] +pub(crate) trait Effect: Send + Sync { type Invocation: EffectInvocation; /// Unique effect identifier. fn id(&self) -> String; - /// Run work associated with effect. - fn run(&self, f: F) - where - F: FnMut(Option::Event>>); + /// Run actual effect implementation. + async fn run(&self) -> Vec<::Event>; /// Cancel any ongoing effect's work. fn cancel(&self); diff --git a/src/core/event_engine/effect_dispatcher.rs b/src/core/event_engine/effect_dispatcher.rs index e411db14..c39047bc 100644 --- a/src/core/event_engine/effect_dispatcher.rs +++ b/src/core/event_engine/effect_dispatcher.rs @@ -1,15 +1,17 @@ +use crate::core::runtime::Runtime; use crate::{ core::event_engine::{Effect, EffectHandler, EffectInvocation}, - lib::alloc::{rc::Rc, vec, vec::Vec}, + lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; -use phantom_type::PhantomType; +use async_channel::Receiver; use spin::rwlock::RwLock; /// State machine effects dispatcher. +#[derive(Debug)] #[allow(dead_code)] pub(crate) struct EffectDispatcher where - EI: EffectInvocation, + EI: EffectInvocation + Send + Sync, EH: EffectHandler, EF: Effect, { @@ -24,53 +26,97 @@ where /// State machines may have some effects that are exclusive and can only run /// one type of them at once. The dispatcher handles such effects /// and cancels them when required. - managed: RwLock>>, + managed: Arc>>>, - _invocation: PhantomType, + /// `Effect invocation` handling channel. + /// + /// Channel is used to receive submitted `invocations` for new effects + /// execution. + invocations_channel: Receiver, + + /// Whether dispatcher already started or not. + started: RwLock, } impl EffectDispatcher where - EI: EffectInvocation, - EH: EffectHandler, - EF: Effect, + EI: EffectInvocation + Send + Sync + 'static, + EH: EffectHandler + Send + Sync + 'static, + EF: Effect + 'static, { /// Create new effects dispatcher. - pub fn new(handler: EH) -> Self { + pub fn new(handler: EH, channel: Receiver) -> Self { EffectDispatcher { handler, - managed: RwLock::new(vec![]), - _invocation: Default::default(), + managed: Arc::new(RwLock::new(vec![])), + invocations_channel: channel, + started: RwLock::new(false), } } - /// Dispatch effect associated with `invocation`. - pub fn dispatch(&self, invocation: &EI, mut f: F) + /// Prepare dispatcher for `invocations` processing. + pub fn start(self: &Arc, completion: C, runtime: R) where - F: FnMut(Option>), + R: Runtime + 'static, + C: Fn(Vec<::Event>) + Clone + Send + 'static, { + let mut started_slot = self.started.write(); + let runtime_clone = runtime.clone(); + let cloned_self = self.clone(); + + runtime.spawn(async move { + log::info!("Subscribe engine has started!"); + + loop { + match cloned_self.invocations_channel.recv().await { + Ok(invocation) => { + log::debug!("Received invocation: {}", invocation.id()); + + let effect = cloned_self.dispatch(&invocation); + let task_completition = completion.clone(); + + if let Some(effect) = effect { + log::debug!("Dispatched effect: {}", effect.id()); + let cloned_self = cloned_self.clone(); + + runtime_clone.spawn(async move { + let events = effect.run().await; + + if invocation.managed() { + cloned_self.remove_managed_effect(effect.id()); + } + + task_completition(events); + }); + } + } + Err(err) => { + log::error!("Receive error: {err:?}"); + } + } + } + }); + + *started_slot = true; + } + + /// Dispatch effect associated with `invocation`. + pub fn dispatch(&self, invocation: &EI) -> Option> { if let Some(effect) = self.handler.create(invocation) { - let effect = Rc::new(effect); + let effect = Arc::new(effect); if invocation.managed() { let mut managed = self.managed.write(); managed.push(effect.clone()); } - // Placeholder for effect invocation. - effect.run(|events| { - // Try remove effect from list of managed. - self.remove_managed_effect(&effect); - - // Notify about effect run completion. - // Placeholder for effect events processing (pass to effects handler). - f(events); - }); - } else if invocation.cancelling() { - self.cancel_effect(invocation); + Some(effect) + } else { + if invocation.cancelling() { + self.cancel_effect(invocation); + } - // Placeholder for effect events processing (pass to effects handler). - f(None); + None } } @@ -86,9 +132,10 @@ where } /// Remove managed effect. - fn remove_managed_effect(&self, effect: &EF) { + #[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()) { + if let Some(position) = managed.iter().position(|ef| ef.id() == effect_id) { managed.remove(position); } } @@ -98,8 +145,9 @@ where mod should { use super::*; use crate::core::event_engine::Event; + use std::future::Future; - enum TestEvent {} + struct TestEvent; impl Event for TestEvent { fn id(&self) -> &str { @@ -113,6 +161,7 @@ mod should { Three, } + #[async_trait::async_trait] impl Effect for TestEffect { type Invocation = TestInvocation; @@ -124,14 +173,8 @@ mod should { } } - fn run(&self, mut f: F) - where - F: FnMut(Option>), - { - match self { - Self::Three => {} - _ => f(None), - } + async fn run(&self) -> Vec { + vec![] } fn cancel(&self) { @@ -188,73 +231,66 @@ mod should { } } + #[derive(Clone)] + struct TestRuntime {} + + #[async_trait::async_trait] + impl Runtime for TestRuntime { + fn spawn(&self, _future: impl Future + Send + 'static) + where + R: Send + 'static, + { + // Do nothing. + } + + async fn sleep(self, _delay: u64) { + // Do nothing. + } + } + #[test] - fn run_not_managed_effect() { - let mut called = false; - let dispatcher = EffectDispatcher::new(TestEffectHandler {}); - dispatcher.dispatch(&TestInvocation::One, |_| { - called = true; - }); + fn create_not_managed_effect() { + let (_tx, rx) = async_channel::bounded::(5); + let dispatcher = Arc::new(EffectDispatcher::new(TestEffectHandler {}, rx)); + let effect = dispatcher.dispatch(&TestInvocation::One); - assert!(called, "Expected to call effect for TestInvocation::One"); assert_eq!( dispatcher.managed.read().len(), 0, "Non managed effects shouldn't be stored" ); + + assert!(effect.unwrap().id() == "EFFECT_ONE"); } - #[test] - fn run_managed_effect() { - let mut called = false; - let dispatcher = EffectDispatcher::new(TestEffectHandler {}); - dispatcher.dispatch(&TestInvocation::Two, |_| { - called = true; - }); + #[tokio::test] + async fn create_managed_effect() { + let (_tx, rx) = async_channel::bounded::(5); + let dispatcher = Arc::new(EffectDispatcher::new(TestEffectHandler {}, rx)); + let effect = dispatcher.dispatch(&TestInvocation::Two); - assert!(called, "Expected to call effect for TestInvocation::Two"); assert_eq!( dispatcher.managed.read().len(), - 0, + 1, "Managed effect should be removed on completion" ); + + assert!(effect.unwrap().id() == "EFFECT_TWO"); } #[test] fn cancel_managed_effect() { - let mut called_managed = false; - let mut cancelled_managed = false; - let dispatcher = EffectDispatcher::new(TestEffectHandler {}); - dispatcher.dispatch(&TestInvocation::Three, |_| { - called_managed = true; - }); + let (_tx, rx) = async_channel::bounded::(5); + let dispatcher = Arc::new(EffectDispatcher::new(TestEffectHandler {}, rx)); + dispatcher.dispatch(&TestInvocation::Three); + let cancelation_effect = dispatcher.dispatch(&TestInvocation::CancelThree); - assert!( - !called_managed, - "Expected that effect for TestInvocation::Three won't be called" - ); - assert_eq!( - dispatcher.managed.read().len(), - 1, - "Managed effect shouldn't complete run because doesn't have completion call" - ); - - dispatcher.dispatch(&TestInvocation::CancelThree, |_| { - cancelled_managed = true; - }); - - assert!( - cancelled_managed, - "Expected to call effect for TestInvocation::CancelThree" - ); - assert!( - !called_managed, - "Expected that effect for TestInvocation::Three won't be called" - ); assert_eq!( dispatcher.managed.read().len(), 0, "Managed effect should be cancelled" ); + + assert!(cancelation_effect.is_none()); } } diff --git a/src/core/event_engine/effect_handler.rs b/src/core/event_engine/effect_handler.rs index fbf1d0fa..8271adcb 100644 --- a/src/core/event_engine/effect_handler.rs +++ b/src/core/event_engine/effect_handler.rs @@ -2,8 +2,8 @@ use crate::core::event_engine::{Effect, EffectInvocation}; pub(crate) trait EffectHandler where - I: EffectInvocation, - EF: Effect, + I: EffectInvocation + Send + Sync, + EF: Effect + Send + Sync, { /// Create effect using information of effect `invocation`. fn create(&self, invocation: &I) -> Option; diff --git a/src/core/event_engine/mod.rs b/src/core/event_engine/mod.rs index 2f01568d..ca99339c 100644 --- a/src/core/event_engine/mod.rs +++ b/src/core/event_engine/mod.rs @@ -1,5 +1,8 @@ //! Event Engine module +use crate::lib::alloc::sync::Arc; +use async_channel::Sender; +use log::error; use spin::rwlock::RwLock; #[doc(inline)] @@ -26,26 +29,34 @@ pub(crate) mod event; pub(crate) use state::State; pub(crate) mod state; +use crate::core::runtime::Runtime; #[doc(inline)] pub(crate) use transition::Transition; + pub(crate) mod transition; /// State machine's event engine. /// /// [`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, + EI: EffectInvocation + Send + Sync, EH: EffectHandler, EF: Effect, - S: State, + S: State + Send + Sync, { /// Effects dispatcher. /// /// Dispatcher responsible for effects invocation processing. - effect_dispatcher: EffectDispatcher, + effect_dispatcher: Arc>, + + /// `Effect invocation` submission channel. + /// + /// Channel is used to submit `invocations` for new effects execution. + effect_dispatcher_channel: Sender, /// Current event engine state. current_state: RwLock, @@ -53,18 +64,30 @@ where impl EventEngine where - EI: EffectInvocation, - EH: EffectHandler, - EF: Effect, - S: State, + S: State + Send + Sync + 'static, + EH: EffectHandler + Send + Sync + 'static, + EF: Effect + 'static, + EI: EffectInvocation + Send + Sync + 'static, { /// Create [`EventEngine`] with initial state for state machine. #[allow(dead_code)] - pub fn new(handler: EH, state: S) -> Self { - EventEngine { - effect_dispatcher: EffectDispatcher::new(handler), + pub fn new(handler: EH, state: S, runtime: R) -> Arc + where + R: Runtime + 'static, + { + let (channel_tx, channel_rx) = async_channel::bounded::(100); + + let effect_dispatcher = Arc::new(EffectDispatcher::new(handler, channel_rx)); + + let engine = Arc::new(EventEngine { + effect_dispatcher, + effect_dispatcher_channel: channel_tx, current_state: RwLock::new(state), - } + }); + + engine.start(runtime); + + engine } /// Retrieve current engine state. @@ -79,10 +102,15 @@ where /// new state if required. #[allow(dead_code)] pub fn process(&self, event: &EI::Event) { - let state = self.current_state.read(); - if let Some(transition) = state.transition(event) { - drop(state); - self.process_transition(transition); + log::debug!("Processing event: {}", event.id()); + + let transition = { + let state = self.current_state.read(); + state.transition(event) + }; + + if let Some(transition) = transition { + self.process_transition(transition) } } @@ -97,20 +125,40 @@ where *writable_state = transition.state; } - transition.invocations.iter().for_each(|invocation| { - self.effect_dispatcher.dispatch(invocation, |events| { - if let Some(events) = events { - events.iter().for_each(|event| self.process(event)); - } - }); + transition.invocations.into_iter().for_each(|invocation| { + if let Err(error) = self.effect_dispatcher_channel.send_blocking(invocation) { + error!("Unable dispatch invocation: {error:?}") + } }); } + + /// Start state machine. + /// + /// This method is used to start state machine and perform initial State + /// transition. + fn start(self: &Arc, runtime: R) + where + R: Runtime + 'static, + { + let engine_clone = self.clone(); + let dispatcher = self.effect_dispatcher.clone(); + + dispatcher.start( + move |events| { + events.iter().for_each(|event| engine_clone.process(event)); + }, + runtime, + ); + } } #[cfg(test)] mod should { use super::*; - use crate::lib::alloc::{vec, vec::Vec}; + use crate::lib::{ + alloc::{vec, vec::Vec}, + core::future::Future, + }; #[derive(Debug, Clone, PartialEq)] enum TestState { @@ -190,12 +238,14 @@ mod should { } } + #[derive(Debug, PartialEq)] enum TestEffect { One, Two, Three, } + #[async_trait::async_trait] impl Effect for TestEffect { type Invocation = TestInvocation; @@ -207,11 +257,9 @@ mod should { } } - fn run(&self, mut f: F) - where - F: FnMut(Option>), - { - f(None) + async fn run(&self) -> Vec { + // Do nothing. + vec![] } fn cancel(&self) { @@ -262,22 +310,40 @@ mod should { } } - #[test] - fn set_initial_state() { - let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted); + #[derive(Clone)] + struct TestRuntime {} + + #[async_trait::async_trait] + impl Runtime for TestRuntime { + fn spawn(&self, future: impl Future + Send + 'static) + where + R: Send + 'static, + { + tokio::spawn(future); + } + + async fn sleep(self, delay: u64) { + tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await + } + } + + #[tokio::test] + async fn set_initial_state() { + let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted, TestRuntime {}); assert!(matches!(engine.current_state(), TestState::NotStarted)); } - #[test] - fn transit_to_new_state() { - let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted); + #[tokio::test] + async fn transit_to_new_state() { + let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted, TestRuntime {}); engine.process(&TestEvent::One); assert!(matches!(engine.current_state(), TestState::Started)); } - #[test] - fn transit_between_states() { - let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted); + #[tokio::test] + // #[ignore = "hangs forever"] + async fn transit_between_states() { + let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted, TestRuntime {}); engine.process(&TestEvent::One); assert!(matches!(engine.current_state(), TestState::Started)); @@ -295,9 +361,9 @@ mod should { )); } - #[test] - fn not_transit_for_unexpected_event() { - let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted); + #[tokio::test] + async fn not_transit_for_unexpected_event() { + let engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted, TestRuntime {}); engine.process(&TestEvent::One); assert!(matches!(engine.current_state(), TestState::Started)); @@ -306,4 +372,9 @@ mod should { assert!(!matches!(engine.current_state(), TestState::Completed)); assert!(matches!(engine.current_state(), TestState::Started)); } + + #[tokio::test] + async fn run_effect() { + let _engine = EventEngine::new(TestEffectHandler {}, TestState::NotStarted, TestRuntime {}); + } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 507e2c20..114e019b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -34,25 +34,23 @@ pub mod transport_request; pub use transport_response::TransportResponse; pub mod transport_response; -#[doc(inline)] -pub use serialize::Serialize; - #[doc(inline)] pub use retry_policy::RequestRetryPolicy; pub mod retry_policy; -#[cfg(any(feature = "publish", feature = "access"))] -pub mod headers; - -pub mod serialize; - #[doc(inline)] pub use deserializer::Deserializer; pub mod deserializer; +#[doc(inline)] +pub use deserialize::Deserialize; +pub mod deserialize; #[doc(inline)] pub use serializer::Serializer; pub mod serializer; +#[doc(inline)] +pub use serialize::Serialize; +pub mod serialize; #[doc(inline)] pub use cryptor::Cryptor; @@ -61,4 +59,13 @@ pub mod cryptor; #[cfg(feature = "event_engine")] pub(crate) mod event_engine; -pub(crate) mod metadata; +#[cfg(feature = "event_engine")] +pub use runtime::Runtime; +#[cfg(feature = "event_engine")] +pub mod runtime; + +pub(crate) mod utils; + +#[doc(inline)] +pub use types::ScalarValue; +pub mod types; diff --git a/src/core/retry_policy.rs b/src/core/retry_policy.rs index 6360120e..b1f42e5c 100644 --- a/src/core/retry_policy.rs +++ b/src/core/retry_policy.rs @@ -8,11 +8,10 @@ //! [`PubNub API`]: https://www.pubnub.com/docs //! [`pubnub`]: ../index.html //! -use crate::core::TransportResponse; +use crate::core::PubNubError; /// Request retry policy. -/// -/// +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum RequestRetryPolicy { /// Requests shouldn't be tried again. None, @@ -20,66 +19,95 @@ pub enum RequestRetryPolicy { /// Retry the request after the same amount of time. Linear { /// The delay between failed retry attempts. - delay: u32, + delay: u64, /// Number of times a request can be retried. - max_retry: u32, + max_retry: u8, }, /// Retry the request using exponential amount of time. Exponential { /// Minimum delay between failed retry attempts. - min_delay: u32, + min_delay: u64, /// Maximum delay between failed retry attempts. - max_delay: u32, + max_delay: u64, /// Number of times a request can be retried. - max_retry: u32, + max_retry: u8, }, } impl RequestRetryPolicy { - #[allow(dead_code)] - pub(crate) fn retry_delay(&self, attempt: &u32, response: &TransportResponse) -> Option { - 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()), - 500..=599 => match self { - RequestRetryPolicy::None => None, - RequestRetryPolicy::Linear { delay, max_retry } => { - (*attempt).le(max_retry).then_some(*delay) - } - RequestRetryPolicy::Exponential { - min_delay, - max_delay, - max_retry, - } => (*attempt) - .le(max_retry) - .then_some((*min_delay).pow(*attempt).min(*max_delay)), - }, - _ => 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) { + return false; + } + + error + .and_then(|e| e.transport_response()) + .map(|response| matches!(response.status, 429 | 500..=599)) + .unwrap_or(!matches!(self, RequestRetryPolicy::None)) + } + + /// 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) { + return None; + } + + error + .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()), + 500..=599 => match self { + Self::None => None, + Self::Linear { delay, .. } => Some(*delay), + Self::Exponential { + min_delay, + max_delay, + .. + } => Some((*min_delay).pow((*attempt).into()).min(*max_delay)), + }, + _ => None, + }) + .unwrap_or(None) + } + + #[cfg(not(feature = "std"))] + pub(crate) fn retry_delay(&self, _attempt: &u8, _error: Option<&PubNubError>) -> Option { + None + } } impl Default for RequestRetryPolicy { fn default() -> Self { - Self::Exponential { - min_delay: 2, - max_delay: 300, - max_retry: 2, - } + Self::None } } #[cfg(test)] mod should { use super::*; - use crate::lib::collections::HashMap; + use crate::{ + core::TransportResponse, + lib::{alloc::boxed::Box, collections::HashMap}, + }; fn client_error_response() -> TransportResponse { TransportResponse { @@ -104,9 +132,9 @@ mod should { } #[test] - fn create_exponential_by_default() { + fn create_none_by_default() { let policy: RequestRetryPolicy = Default::default(); - assert!(matches!(policy, RequestRetryPolicy::Exponential { .. })); + assert!(matches!(policy, RequestRetryPolicy::None)); } mod none_policy { @@ -115,7 +143,14 @@ mod should { #[test] fn return_none_delay_for_client_error_response() { assert_eq!( - RequestRetryPolicy::None.retry_delay(&1, &client_error_response()), + RequestRetryPolicy::None.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(client_error_response())) + )) + ), None ); } @@ -123,7 +158,14 @@ mod should { #[test] fn return_none_delay_for_server_error_response() { assert_eq!( - RequestRetryPolicy::None.retry_delay(&1, &server_error_response()), + RequestRetryPolicy::None.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), None ); } @@ -131,7 +173,14 @@ mod should { #[test] fn return_none_delay_for_too_many_requests_error_response() { assert_eq!( - RequestRetryPolicy::None.retry_delay(&1, &too_many_requests_error_response()), + RequestRetryPolicy::None.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(too_many_requests_error_response())) + )) + ), None ); } @@ -147,42 +196,83 @@ mod should { max_retry: 5, }; - assert_eq!(policy.retry_delay(&1, &client_error_response()), None); + assert_eq!( + policy.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(client_error_response())) + )) + ), + None + ); } #[test] fn return_same_delay_for_server_error_response() { - let expected_delay = 10; + let expected_delay: u64 = 10; let policy = RequestRetryPolicy::Linear { delay: expected_delay, max_retry: 5, }; assert_eq!( - policy.retry_delay(&1, &server_error_response()), + policy.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay) ); assert_eq!( - policy.retry_delay(&2, &server_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay) ); } #[test] fn return_none_delay_when_reach_max_retry_for_server_error_response() { - let expected_delay = 10; + let expected_delay: u64 = 10; let policy = RequestRetryPolicy::Linear { delay: expected_delay, - max_retry: 2, + max_retry: 3, }; assert_eq!( - policy.retry_delay(&2, &server_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay) ); - assert_eq!(policy.retry_delay(&3, &server_error_response()), None); + assert_eq!( + policy.retry_delay( + &4, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), + None + ); } #[test] @@ -194,7 +284,14 @@ mod should { // 150 is from 'server_error_response' `Retry-After` header. assert_eq!( - policy.retry_delay(&2, &too_many_requests_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(too_many_requests_error_response())) + )) + ), Some(150) ); } @@ -212,7 +309,17 @@ mod should { max_retry: 2, }; - assert_eq!(policy.retry_delay(&1, &client_error_response()), None); + assert_eq!( + policy.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(client_error_response())) + )) + ), + None + ); } #[test] @@ -221,16 +328,30 @@ mod should { let policy = RequestRetryPolicy::Exponential { min_delay: expected_delay, max_delay: 100, - max_retry: 2, + max_retry: 3, }; assert_eq!( - policy.retry_delay(&1, &server_error_response()), + policy.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay) ); assert_eq!( - policy.retry_delay(&2, &server_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay.pow(2)) ); } @@ -241,15 +362,32 @@ mod should { let policy = RequestRetryPolicy::Exponential { min_delay: expected_delay, max_delay: 100, - max_retry: 2, + max_retry: 3, }; assert_eq!( - policy.retry_delay(&2, &server_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay.pow(2)) ); - assert_eq!(policy.retry_delay(&3, &server_error_response()), None); + assert_eq!( + policy.retry_delay( + &4, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), + None + ); } #[test] @@ -263,12 +401,26 @@ mod should { }; assert_eq!( - policy.retry_delay(&1, &server_error_response()), + policy.retry_delay( + &1, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(expected_delay) ); assert_eq!( - policy.retry_delay(&2, &server_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(server_error_response())) + )) + ), Some(max_delay) ); } @@ -283,7 +435,14 @@ mod should { // 150 is from 'server_error_response' `Retry-After` header. assert_eq!( - policy.retry_delay(&2, &too_many_requests_error_response()), + policy.retry_delay( + &2, + Some(&PubNubError::general_api_error( + "test", + None, + Some(Box::new(too_many_requests_error_response())) + )) + ), Some(150) ); } diff --git a/src/core/runtime.rs b/src/core/runtime.rs new file mode 100644 index 00000000..a628a18c --- /dev/null +++ b/src/core/runtime.rs @@ -0,0 +1,47 @@ +//! This module contains the task spawning trait used in the PubNub client. +//! +//! The [`Spawner`] trait is used to spawn async tasks in work of the PubNub +//! client. + +use crate::lib::{alloc::boxed::Box, core::future::Future}; + +/// PubNub spawner trait. +/// +/// This trait is used to spawn async tasks in work of the PubNub client. +/// It is used to spawn tasks for the proper work of some features +/// that require async tasks to be spawned. +/// +/// # Examples +/// ``` +/// use pubnub::core::{Runtime, PubNubError}; +/// use std::future::Future; +/// +/// #[derive(Clone)] +/// struct MyRuntime; +/// +/// #[async_trait::async_trait] +/// impl Runtime for MyRuntime { +/// fn spawn(&self, future: impl Future + Send + 'static) { +/// // spawn the Future +/// // e.g. tokio::spawn(future); +/// } +/// +/// async fn sleep(self, _delay: u64) { +/// // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await +/// } +/// } +/// ``` +#[async_trait::async_trait] +pub trait Runtime: Clone + Send { + /// Spawn a task. + /// + /// This method is used to spawn a task. + fn spawn(&self, future: impl Future + Send + 'static) + where + R: Send + 'static; + + /// Put current task to "sleep". + /// + /// Sleep current task for specified amount of time (in seconds). + async fn sleep(self, delay: u64); +} diff --git a/src/core/types.rs b/src/core/types.rs new file mode 100644 index 00000000..807fd198 --- /dev/null +++ b/src/core/types.rs @@ -0,0 +1,139 @@ +//! # Common PubNub module tyoes +//! +//! The module contains [`ScalarValue`] type used in various operations and data types. + +use crate::lib::alloc::string::String; + +/// Scalar values for flattened [`HashMap`]. +/// +/// Some endpoints only require [`HashMap`], which should not have nested +/// collections. This requirement is implemented through this type. +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +pub enum ScalarValue { + /// `String` value stored for specific key in [`HashMap`]. + String(String), + + /// `Boolean` value stored for specific key in [`HashMap`]. + Boolean(bool), + + /// `signed 8-bit` value stored for specific key in [`HashMap`]. + Signed8(i8), + + /// `unsigned 8-bit` value stored for specific key in [`HashMap`]. + Unsigned8(u8), + + /// `signed 16-bit` value stored for specific key in [`HashMap`]. + Signed16(i16), + + /// `unsigned 16-bit` value stored for specific key in [`HashMap`]. + Unsigned16(u16), + + /// `signed 32-bit` value stored for specific key in [`HashMap`]. + Signed32(i32), + + /// `unsigned 32-bit` value stored for specific key in [`HashMap`]. + Unsigned32(u32), + + /// `signed 64-bit` value stored for specific key in [`HashMap`]. + Signed64(i64), + + /// `unsigned 64-bit` value stored for specific key in [`HashMap`]. + Unsigned64(u64), + + /// `signed 128-bit` value stored for specific key in [`HashMap`]. + Signed128(i128), + + /// `unsigned 128-bit` value stored for specific key in [`HashMap`]. + Unsigned128(u128), + + /// `32-bit floating point` value stored for specific key in [`HashMap`]. + Float32(f32), + + /// `64-bit floating point` value stored for specific key in [`HashMap`]. + Float64(f64), +} + +impl From for ScalarValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From for ScalarValue { + fn from(value: bool) -> Self { + Self::Boolean(value) + } +} + +impl From for ScalarValue { + fn from(value: i8) -> Self { + Self::Signed8(value) + } +} + +impl From for ScalarValue { + fn from(value: u8) -> Self { + Self::Unsigned8(value) + } +} + +impl From for ScalarValue { + fn from(value: i16) -> Self { + Self::Signed16(value) + } +} + +impl From for ScalarValue { + fn from(value: u16) -> Self { + Self::Unsigned16(value) + } +} + +impl From for ScalarValue { + fn from(value: i32) -> Self { + Self::Signed32(value) + } +} + +impl From for ScalarValue { + fn from(value: u32) -> Self { + Self::Unsigned32(value) + } +} + +impl From for ScalarValue { + fn from(value: i64) -> Self { + Self::Signed64(value) + } +} + +impl From for ScalarValue { + fn from(value: u64) -> Self { + Self::Unsigned64(value) + } +} + +impl From for ScalarValue { + fn from(value: i128) -> Self { + Self::Signed128(value) + } +} + +impl From for ScalarValue { + fn from(value: u128) -> Self { + Self::Unsigned128(value) + } +} + +impl From for ScalarValue { + fn from(value: f32) -> Self { + Self::Float32(value) + } +} + +impl From for ScalarValue { + fn from(value: f64) -> Self { + Self::Float64(value) + } +} diff --git a/src/core/utils/encoding.rs b/src/core/utils/encoding.rs new file mode 100644 index 00000000..b33eaca4 --- /dev/null +++ b/src/core/utils/encoding.rs @@ -0,0 +1,72 @@ +use crate::lib::alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; + +/// https://url.spec.whatwg.org/#fragment-percent-encode-set +const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); + +/// https://url.spec.whatwg.org/#path-percent-encode-set +const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); + +/// https://url.spec.whatwg.org/#userinfo-percent-encode-set +const USERINFO: &AsciiSet = &PATH + .add(b'/') + .add(b':') + .add(b';') + .add(b'=') + .add(b'@') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'|'); + +/// `+` sign needed by PubNub API +const PUBNUB_SET: &AsciiSet = &USERINFO.add(b'+').add(b'%').add(b'!').add(b'$'); + +/// Additional non-channel path component extension. +const PUBNUB_NON_CHANNEL_PATH: &AsciiSet = &PUBNUB_SET.add(b','); + +pub enum UrlEncodeExtension { + /// Default PubNub required encoding. + Default, + + /// Encoding applied to any non-channel component in path. + NonChannelPath, +} + +/// `percent_encoding` crate recommends you to create your own set for encoding. +/// To be consistent in the whole codebase - we created a function that can be used +/// for encoding related stuff. +pub fn url_encode(data: &[u8]) -> String { + url_encode_extended(data, UrlEncodeExtension::Default).to_string() +} + +/// `percent_encoding` crate recommends you to create your own set for encoding. +/// To be consistent in the whole codebase - we created a function that can be used +/// for encoding related stuff. +pub fn url_encode_extended(data: &[u8], extension: UrlEncodeExtension) -> String { + let set = match extension { + UrlEncodeExtension::Default => PUBNUB_SET, + UrlEncodeExtension::NonChannelPath => PUBNUB_NON_CHANNEL_PATH, + }; + + percent_encode(data, set).to_string() +} + +/// Join list of encoded strings. +pub fn join_url_encoded(strings: &[&str], sep: &str) -> Option { + if strings.is_empty() { + return None; + } + + Some( + strings + .iter() + .map(|val| url_encode(val.as_bytes())) + .collect::>() + .join(sep), + ) +} diff --git a/src/core/headers.rs b/src/core/utils/headers.rs similarity index 100% rename from src/core/headers.rs rename to src/core/utils/headers.rs diff --git a/src/core/metadata.rs b/src/core/utils/metadata.rs similarity index 100% rename from src/core/metadata.rs rename to src/core/utils/metadata.rs diff --git a/src/core/utils/mod.rs b/src/core/utils/mod.rs new file mode 100644 index 00000000..28a771f6 --- /dev/null +++ b/src/core/utils/mod.rs @@ -0,0 +1,6 @@ +#[cfg(any(feature = "publish", feature = "access"))] +pub mod encoding; +#[cfg(any(feature = "publish", feature = "access"))] +pub mod headers; + +pub mod metadata; diff --git a/src/dx/access/builders/grant_token.rs b/src/dx/access/builders/grant_token.rs index 9064e157..5fe96dee 100644 --- a/src/dx/access/builders/grant_token.rs +++ b/src/dx/access/builders/grant_token.rs @@ -5,12 +5,12 @@ use crate::{ core::{ error::PubNubError, - headers::{APPLICATION_JSON, CONTENT_TYPE}, + utils::headers::{APPLICATION_JSON, CONTENT_TYPE}, Deserializer, Serializer, Transport, TransportMethod, TransportRequest, }, dx::{access::*, pubnub_client::PubNubClientInstance}, lib::{ - alloc::{format, string::ToString, vec}, + alloc::{boxed::Box, format, string::ToString, vec}, collections::HashMap, }, }; @@ -34,7 +34,7 @@ use derive_builder::Builder; pub struct GrantTokenRequest<'pa, T, S, D> where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: for<'dl> Deserializer<'dl, GrantTokenResponseBody>, + D: Deserializer, { /// Current client which can provide transportation to perform the request. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] @@ -132,7 +132,7 @@ where impl<'pa, T, S, D> GrantTokenRequest<'pa, T, S, D> where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, { /// Create transport request from the request builder. pub(in crate::dx::access) fn transport_request(&self) -> TransportRequest { @@ -153,7 +153,7 @@ where impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, { /// Validate user-provided data for request builder. /// @@ -168,32 +168,47 @@ impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where T: Transport, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, { /// Build and call request. pub async fn execute(self) -> Result { // Build request instance and report errors if any. let request = self .build() - .map_err(|err| PubNubError::general_api_error(err.to_string(), None))?; + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = request.deserializer; - client - .transport - .send(transport_request) - .await? + let response = client.transport.send(transport_request).await?; + response + .clone() .body - .map(|bytes| deserializer.deserialize(&bytes)) + .map(|bytes| { + let deserialize_result = deserializer.deserialize(&bytes); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())), + )) + } else { + deserialize_result + } + }) .map_or( Err(PubNubError::general_api_error( "No body in the response!", None, + Some(Box::new(response.clone())), )), |response_body| { - response_body.and_then::(|body| body.try_into()) + response_body.and_then::(|body| { + body.try_into().map_err(|response_error: PubNubError| { + response_error.attach_response(response) + }) + }) }, ) } @@ -204,7 +219,7 @@ impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where T: crate::core::blocking::Transport, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, { /// Execute the request and return the result. /// @@ -243,24 +258,40 @@ where // Build request instance and report errors if any. let request = self .build() - .map_err(|err| PubNubError::general_api_error(err.to_string(), None))?; + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = request.deserializer; - client - .transport - .send(transport_request)? + let response = client.transport.send(transport_request)?; + response .body - .map(|bytes| deserializer.deserialize(&bytes)) + .as_ref() + .map(|bytes| { + let deserialize_result = deserializer.deserialize(bytes); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())), + )) + } else { + deserialize_result + } + }) .map_or( Err(PubNubError::general_api_error( "No body in the response!", None, + Some(Box::new(response.clone())), )), |response_body| { - response_body.and_then::(|body| body.try_into()) + response_body.and_then::(|body| { + body.try_into().map_err(|response_error: PubNubError| { + response_error.attach_response(response) + }) + }) }, ) } @@ -278,7 +309,7 @@ impl GrantTokenRequestWithSerializerBuilder { serializer: S, ) -> GrantTokenRequestWithDeserializerBuilder where - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, { GrantTokenRequestWithDeserializerBuilder { @@ -304,7 +335,7 @@ where deserializer: D, ) -> GrantTokenRequestBuilder<'builder, T, S, D> where - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, { GrantTokenRequestBuilder { pubnub_client: Some(self.pubnub_client), diff --git a/src/dx/access/builders/revoke.rs b/src/dx/access/builders/revoke.rs index 6165a36a..4708bdb7 100644 --- a/src/dx/access/builders/revoke.rs +++ b/src/dx/access/builders/revoke.rs @@ -5,14 +5,14 @@ use crate::{ core::{ error::PubNubError, - headers::{APPLICATION_JSON, CONTENT_TYPE}, + utils::{ + encoding::url_encode, + headers::{APPLICATION_JSON, CONTENT_TYPE}, + }, Deserializer, Transport, TransportMethod, TransportRequest, }, dx::{access::*, pubnub_client::PubNubClientInstance}, - lib::{ - alloc::{format, string::ToString}, - encoding::url_encode, - }, + lib::alloc::{boxed::Box, format, string::ToString}, }; use derive_builder::Builder; @@ -34,7 +34,7 @@ use derive_builder::Builder; /// [`PubNubClient`]: crate::PubNubClient pub struct RevokeTokenRequest where - D: for<'de> Deserializer<'de, RevokeTokenResponseBody>, + D: Deserializer, { /// Current client which can provide transportation to perform the request. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] @@ -69,7 +69,7 @@ pub struct RevokeTokenRequestWithDeserializerBuilder { impl RevokeTokenRequest where - D: for<'de> Deserializer<'de, RevokeTokenResponseBody>, + D: Deserializer, { /// Create transport request from the request builder. pub(in crate::dx::access) fn transport_request(&self) -> TransportRequest { @@ -89,7 +89,7 @@ where impl RevokeTokenRequestBuilder where - D: for<'de> Deserializer<'de, RevokeTokenResponseBody>, + D: Deserializer, { /// Validate user-provided data for request builder. /// @@ -103,32 +103,47 @@ where impl RevokeTokenRequestBuilder where T: Transport, - D: for<'de> Deserializer<'de, RevokeTokenResponseBody>, + D: Deserializer, { /// Build and call request. pub async fn execute(self) -> Result { // Build request instance and report errors if any. let request = self .build() - .map_err(|err| PubNubError::general_api_error(err.to_string(), None))?; + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = request.deserializer; - client - .transport - .send(transport_request) - .await? + let response = client.transport.send(transport_request).await?; + response + .clone() .body - .map(|bytes| deserializer.deserialize(&bytes)) + .map(|bytes| { + let deserialize_result = deserializer.deserialize(&bytes); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())), + )) + } else { + deserialize_result + } + }) .map_or( Err(PubNubError::general_api_error( "No body in the response!", None, + Some(Box::new(response.clone())), )), |response_body| { - response_body.and_then::(|body| body.try_into()) + response_body.and_then::(|body| { + body.try_into().map_err(|response_error: PubNubError| { + response_error.attach_response(response) + }) + }) }, ) } @@ -138,7 +153,7 @@ where impl RevokeTokenRequestBuilder where T: crate::core::blocking::Transport, - D: for<'de> Deserializer<'de, RevokeTokenResponseBody>, + D: Deserializer, { /// Execute the request and return the result. /// @@ -170,24 +185,40 @@ where // Build request instance and report errors if any. let request = self .build() - .map_err(|err| PubNubError::general_api_error(err.to_string(), None))?; + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); let deserializer = request.deserializer; - client - .transport - .send(transport_request)? + let response = client.transport.send(transport_request)?; + response .body - .map(|bytes| deserializer.deserialize(&bytes)) + .as_ref() + .map(|bytes| { + let deserialize_result = deserializer.deserialize(bytes); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())), + )) + } else { + deserialize_result + } + }) .map_or( Err(PubNubError::general_api_error( "No body in the response!", None, + Some(Box::new(response.clone())), )), |response_body| { - response_body.and_then::(|body| body.try_into()) + response_body.and_then::(|body| { + body.try_into().map_err(|response_error: PubNubError| { + response_error.attach_response(response) + }) + }) }, ) } @@ -202,7 +233,7 @@ impl RevokeTokenRequestWithDeserializerBuilder { /// Instance of [`RevokeTokenRequestBuilder`] returned. pub fn deserialize_with(self, deserializer: D) -> RevokeTokenRequestBuilder where - D: for<'de> Deserializer<'de, RevokeTokenResponseBody>, + D: Deserializer, { RevokeTokenRequestBuilder { pubnub_client: Some(self.pubnub_client), diff --git a/src/dx/access/mod.rs b/src/dx/access/mod.rs index 665388ab..208b9de3 100644 --- a/src/dx/access/mod.rs +++ b/src/dx/access/mod.rs @@ -34,7 +34,7 @@ pub use permissions::*; pub mod permissions; use crate::dx::pubnub_client::PubNubClientInstance; -use crate::lib::alloc::{boxed::Box, string::String}; +use crate::lib::alloc::string::String; #[cfg(feature = "serde")] use crate::providers::{ deserialization_serde::SerdeDeserializer, serialization_serde::SerdeSerializer, diff --git a/src/dx/access/payloads.rs b/src/dx/access/payloads.rs index 5406274c..9d90b4d4 100644 --- a/src/dx/access/payloads.rs +++ b/src/dx/access/payloads.rs @@ -82,7 +82,7 @@ impl<'request> GrantTokenPayload<'request> { pub(super) fn new(request: &'request GrantTokenRequest<'_, T, S, D>) -> Self where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: for<'ds> Deserializer<'ds, GrantTokenResponseBody>, + D: Deserializer, { GrantTokenPayload { ttl: request.ttl, diff --git a/src/dx/access/result.rs b/src/dx/access/result.rs index 47707588..b432a9ef 100644 --- a/src/dx/access/result.rs +++ b/src/dx/access/result.rs @@ -39,7 +39,7 @@ pub enum GrantTokenResponseBody { /// It contains information about the service that gave the response and the /// data with the token that was generated. /// - /// # Example + /// # Example /// ```json /// { /// "status": 200, diff --git a/src/dx/parse_token.rs b/src/dx/parse_token.rs index 184d6042..d4322a2d 100644 --- a/src/dx/parse_token.rs +++ b/src/dx/parse_token.rs @@ -12,7 +12,6 @@ use crate::{ string::{String, ToString}, }, collections::HashMap, - core::ops::Deref, }, }; use base64::{engine::general_purpose, Engine}; @@ -23,8 +22,10 @@ use ciborium::de::from_reader; struct CiboriumDeserializer; #[cfg(feature = "serde")] -impl<'de> Deserializer<'de, Token> for CiboriumDeserializer { - fn deserialize(&self, bytes: &'de [u8]) -> Result { +impl Deserializer for CiboriumDeserializer { + fn deserialize(&self, bytes: &[u8]) -> Result { + use crate::lib::core::ops::Deref; + from_reader(bytes.deref()).map_err(|e| PubNubError::TokenDeserialization { details: e.to_string(), }) @@ -46,7 +47,7 @@ pub fn parse_token(token: &str) -> Result { /// resources. pub fn parse_token_with(token: &str, deserializer: D) -> Result where - D: for<'de> Deserializer<'de, Token>, + D: Deserializer, { let token_bytes = general_purpose::URL_SAFE .decode(format!("{token}{}", "=".repeat(token.len() % 4)).as_bytes()) @@ -54,7 +55,7 @@ where details: e.to_string(), })?; - deserializer.deserialize(token_bytes.deref()) + deserializer.deserialize(&token_bytes) } /// Version based access token. @@ -198,7 +199,10 @@ pub enum MetaValue { #[cfg(test)] mod should { use super::*; - use crate::dx::parse_token::MetaValue::{Float, Integer, Null, String}; + use crate::{ + dx::parse_token::MetaValue::{Float, Integer, Null, String}, + lib::core::ops::Deref, + }; impl PartialEq for MetaValue { fn eq(&self, other: &Self) -> bool { diff --git a/src/dx/publish/builders.rs b/src/dx/publish/builders.rs index 41a0e31d..337a34cc 100644 --- a/src/dx/publish/builders.rs +++ b/src/dx/publish/builders.rs @@ -107,7 +107,7 @@ where /// ```rust /// # use pubnub::{PubNubClientBuilder, Keyset}; /// use pubnub::{ -/// dx::publish::PublishResponse, +/// dx::publish::{PublishResponse, PublishResponseBody}, /// core::{Deserializer, PubNubError} /// }; /// # #[tokio::main] @@ -115,13 +115,12 @@ where /// /// struct MyDeserializer; /// -/// impl<'de> Deserializer<'de, PublishResponseBody> for MyDeserializer { -/// fn deserialize(&self, response: &'de [u8]) -> Result { +/// impl Deserializer for MyDeserializer { +/// fn deserialize(&self, response: &Vec) -> Result { /// // ... /// # Ok(PublishResponse) /// } /// -/// /// let mut pubnub = // PubNubClient /// # PubNubClientBuilder::with_reqwest_transport() /// # .with_keyset(Keyset{ @@ -170,7 +169,7 @@ where /// [`PublishResponseBody`]: crate::core::publish::PublishResponseBody pub fn deserialize_with(self, deserializer: D) -> PublishMessageViaChannelBuilder where - for<'de> D: Deserializer<'de, PublishResponseBody>, + D: Deserializer, { PublishMessageViaChannelBuilder { pub_nub_client: Some(self.pub_nub_client), @@ -221,7 +220,7 @@ where pub struct PublishMessageViaChannel where M: Serialize, - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { #[builder(setter(custom))] pub(super) pub_nub_client: PubNubClientInstance, diff --git a/src/dx/publish/mod.rs b/src/dx/publish/mod.rs index 24b9c6e2..d6323ea1 100644 --- a/src/dx/publish/mod.rs +++ b/src/dx/publish/mod.rs @@ -15,30 +15,35 @@ pub use result::{PublishResponseBody, PublishResult}; pub mod result; #[doc(inline)] -pub use builders::PublishMessageBuilder; +pub use builders::{ + PublishMessageBuilder, PublishMessageViaChannel, PublishMessageViaChannelBuilder, +}; pub mod builders; use self::result::body_to_result; -use super::pubnub_client::PubNubConfig; use crate::{ core::{ - headers::{APPLICATION_JSON, CONTENT_TYPE}, - Deserializer, PubNubError, Serialize, Transport, TransportMethod, TransportRequest, - TransportResponse, + utils::{ + encoding::{url_encode, url_encode_extended, UrlEncodeExtension}, + headers::{APPLICATION_JSON, CONTENT_TYPE}, + }, + Cryptor, Deserializer, PubNubError, Serialize, Transport, TransportMethod, + TransportRequest, TransportResponse, }, - dx::pubnub_client::PubNubClientInstance, + dx::pubnub_client::{PubNubClientInstance, PubNubConfig}, lib::{ alloc::{ + boxed::Box, format, string::{String, ToString}, + sync::Arc, }, collections::HashMap, core::ops::Not, - encoding::url_encode, }, }; -use builders::{PublishMessageViaChannel, PublishMessageViaChannelBuilder}; +use base64::{engine::general_purpose, Engine as _}; impl PubNubClientInstance { /// Create a new publish message builder. @@ -100,17 +105,19 @@ impl PubNubClientInstance { impl PublishMessageViaChannelBuilder where M: Serialize, - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { fn prepare_context_with_request( self, ) -> Result, PubNubError> { let instance = self .build() - .map_err(|err| PubNubError::general_api_error(err.to_string(), None))?; + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; PublishMessageContext::from(instance) - .map_data(|client, _, params| params.create_transport_request(&client.config)) + .map_data(|client, _, params| { + params.create_transport_request(&client.config, &client.cryptor.clone()) + }) .map(|ctx| { Ok(PublishMessageContext { client: ctx.client, @@ -125,7 +132,7 @@ impl PublishMessageViaChannelBuilder where T: Transport, M: Serialize, - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { /// Execute the request and return the result. /// This method is asynchronous and will return a future. @@ -169,7 +176,7 @@ where } }) .await - .map_data(|_, deserializer, resposne| response_to_result(deserializer, resposne?)) + .map_data(|_, deserializer, response| response_to_result(deserializer, response?)) .data } @@ -186,7 +193,7 @@ impl PublishMessageViaChannelBuilder where T: crate::core::blocking::Transport, M: Serialize, - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { /// Execute the request and return the result. /// This method is asynchronous and will return a future. @@ -271,17 +278,25 @@ where fn create_transport_request( self, config: &PubNubConfig, + cryptor: &Option>, ) -> Result { let query_params = self.prepare_publish_query_params(); let pub_key = config .publish_key .as_ref() - .ok_or_else(|| PubNubError::general_api_error("Publish key is not set", None))?; + .ok_or_else(|| PubNubError::general_api_error("Publish key is not set", None, None))?; let sub_key = &config.subscribe_key; + let mut m_vec = self.message.serialize()?; + if let Some(cryptor) = cryptor { + if let Ok(encrypted) = cryptor.encrypt(m_vec.to_vec()) { + m_vec = format!("\"{}\"", general_purpose::STANDARD.encode(encrypted)).into_bytes(); + } + } + if self.use_post { - self.message.serialize().map(|m_vec| TransportRequest { + Ok(TransportRequest { path: format!( "/publish/{pub_key}/{sub_key}/0/{}/0", url_encode(self.channel.as_bytes()) @@ -292,12 +307,9 @@ where headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), }) } else { - self.message - .serialize() - .and_then(|m_vec| { - String::from_utf8(m_vec).map_err(|e| PubNubError::Serialization { - details: e.to_string(), - }) + String::from_utf8(m_vec) + .map_err(|e| PubNubError::Serialization { + details: e.to_string(), }) .map(|m_str| TransportRequest { path: format!( @@ -305,7 +317,7 @@ where pub_key, sub_key, url_encode(self.channel.as_bytes()), - url_encode(m_str.as_bytes()) + url_encode_extended(m_str.as_bytes(), UrlEncodeExtension::NonChannelPath) ), method: TransportMethod::Get, query_parameters: query_params, @@ -325,7 +337,7 @@ impl From> for PublishMessageContext> where M: Serialize, - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { fn from(value: PublishMessageViaChannel) -> Self { Self { @@ -349,7 +361,7 @@ where impl PublishMessageContext where - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { fn map_data(self, f: F) -> PublishMessageContext where @@ -412,30 +424,43 @@ fn response_to_result( response: TransportResponse, ) -> Result where - D: for<'de> Deserializer<'de, PublishResponseBody>, + D: Deserializer, { response .body - .map(|body| deserializer.deserialize(&body)) + .as_ref() + .map(|body| { + let deserialize_result = deserializer.deserialize(body); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())), + )) + } else { + deserialize_result + } + }) .transpose() .and_then(|body| { body.ok_or_else(|| { PubNubError::general_api_error( format!("No body in the response! Status code: {}", response.status), None, + Some(Box::new(response.clone())), ) }) - .map(|body| body_to_result(body, response.status)) + .map(|body| body_to_result(body, response)) })? } #[cfg(test)] mod should { use super::*; - use crate::lib::alloc::{boxed::Box, sync::Arc, vec}; use crate::{ core::TransportResponse, dx::pubnub_client::{PubNubClientInstance, PubNubClientRef, PubNubConfig}, + lib::alloc::{sync::Arc, vec}, transport::middleware::PubNubMiddleware, Keyset, PubNubClientBuilder, }; @@ -459,7 +484,7 @@ mod should { } } - PubNubClientBuilder::with_transport(MockTransport::default()) + PubNubClientBuilder::with_transport(MockTransport) .with_keyset(Keyset { publish_key: Some(""), subscribe_key: "", @@ -489,7 +514,7 @@ mod should { let result = client .publish_message("First message") - .channel("Iguess") + .channel("IGuess") .replicate(true) .execute() .await; @@ -531,19 +556,18 @@ mod should { fn verify_seqn_is_incrementing() { let client = client(); - let received_seqns = vec![ - client.publish_message("meess").seqn, - client.publish_message("meess").seqn, + let received_sequence_numbers = vec![ + client.publish_message("message").seqn, + client.publish_message("message").seqn, ]; - assert_eq!(vec![1, 2], received_seqns); + 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 { @@ -558,7 +582,7 @@ mod should { }; assert!(client - .publish_message("meess") + .publish_message("message") .channel("chan") .execute() .await @@ -581,7 +605,10 @@ mod should { format!( "/publish///0/{}/0/{}", channel, - url_encode(format!("\"{}\"", message).as_bytes()) + url_encode_extended( + format!("\"{}\"", message).as_bytes(), + UrlEncodeExtension::NonChannelPath + ) ), result.data.path ); @@ -603,7 +630,10 @@ mod should { format!( "/publish///0/{}/0/{}", channel, - url_encode("{\"a\":\"b\"}".as_bytes()) + url_encode_extended( + "{\"a\":\"b\"}".as_bytes(), + UrlEncodeExtension::NonChannelPath + ) ), result.data.path ); @@ -646,7 +676,10 @@ mod should { format!( "/publish///0/{}/0/{}", channel, - url_encode("{\"number\":7}".as_bytes()) + url_encode_extended( + "{\"number\":7}".as_bytes(), + UrlEncodeExtension::NonChannelPath + ) ), result.data.path ); @@ -696,7 +729,7 @@ mod should { } } - let client = PubNubClientBuilder::with_transport(MockTransport::default()) + let client = PubNubClientBuilder::with_transport(MockTransport) .with_keyset(Keyset { publish_key: Some(""), subscribe_key: "", diff --git a/src/dx/publish/result.rs b/src/dx/publish/result.rs index 826b3d7b..8475fc83 100644 --- a/src/dx/publish/result.rs +++ b/src/dx/publish/result.rs @@ -4,8 +4,8 @@ //! The `PublishResult` type is used to represent the result of a publish operation. use crate::{ - core::{APIErrorBody, PubNubError}, - lib::alloc::string::String, + core::{APIErrorBody, PubNubError, TransportResponse}, + lib::alloc::{boxed::Box, string::String}, }; /// The result of a publish operation. @@ -46,17 +46,24 @@ pub enum PublishResponseBody { pub(super) fn body_to_result( body: PublishResponseBody, - status: u16, + response: TransportResponse, ) -> Result { match body { PublishResponseBody::SuccessResponse(error_indicator, message, timetoken) => { if error_indicator == 1 { Ok(PublishResult { timetoken }) } else { - Err(PubNubError::general_api_error(message, Some(status))) + Err(PubNubError::general_api_error( + message, + Some(response.status), + Some(Box::new(response)), + )) } } - PublishResponseBody::ErrorResponse(resp) => Err(resp.into()), + PublishResponseBody::ErrorResponse(resp) => { + let error: PubNubError = resp.into(); + Err(error.attach_response(response)) + } } } @@ -68,7 +75,14 @@ mod should { fn parse_publish_response() { let body = PublishResponseBody::SuccessResponse(1, "Sent".into(), "15815800000000000".into()); - let result = body_to_result(body, 200).unwrap(); + let result = body_to_result( + body, + TransportResponse { + status: 200, + ..Default::default() + }, + ) + .unwrap(); assert_eq!(result.timetoken, "15815800000000000"); } @@ -82,7 +96,13 @@ mod should { service: "service".into(), message: "error".into(), }); - let result = body_to_result(body, status); + let result = body_to_result( + body, + TransportResponse { + status, + ..Default::default() + }, + ); assert!(result.is_err()); } diff --git a/src/dx/pubnub_client.rs b/src/dx/pubnub_client.rs index 4b0fcd9e..229925b2 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -7,19 +7,23 @@ //! [`PubNub API`]: https://www.pubnub.com/docs //! [`pubnub`]: ../index.html -use crate::transport::middleware::SignatureKeySet; +use crate::core::Cryptor; +#[cfg(feature = "subscribe")] +use crate::dx::subscribe::SubscriptionConfiguration; use crate::{ - core::{PubNubError, Transport}, - lib::alloc::{ - string::{String, ToString}, - sync::Arc, + core::{PubNubError, RequestRetryPolicy, Transport}, + lib::{ + alloc::{ + string::{String, ToString}, + sync::Arc, + }, + core::ops::{Deref, DerefMut}, }, - lib::core::ops::Deref, - transport::middleware::PubNubMiddleware, + transport::middleware::{PubNubMiddleware, SignatureKeySet}, }; use derive_builder::Builder; use log::info; -use spin::Mutex; +use spin::{Mutex, RwLock}; /// PubNub client /// @@ -71,7 +75,7 @@ use spin::Mutex; /// # } /// # } /// -/// # fn main() -> Result<(), pubnub::core::PubNubError> { +/// # fn main() -> Result<(), PubNubError> { /// // note that MyTransport must implement the `Transport` trait /// let transport = MyTransport::new(); /// @@ -152,7 +156,7 @@ pub type PubNubGenericClient = PubNubClientInstance>; /// # } /// # } /// -/// # fn main() -> Result<(), pubnub::core::PubNubError> { +/// # fn main() -> Result<(), PubNubError> { /// // note that MyTransport must implement the `Transport` trait /// let transport = MyTransport::new(); /// @@ -191,7 +195,6 @@ pub type PubNubClient = PubNubGenericClient; /// /// This struct contains the actual client state. /// It shouldn't be used directly. Use [`PubNubGenericClient`] or [`PubNubClient`] instead. -#[derive(Debug)] pub struct PubNubClientInstance { pub(crate) inner: Arc>, } @@ -204,6 +207,13 @@ impl Deref for PubNubClientInstance { } } +impl DerefMut for PubNubClientInstance { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner) + .expect("Multiple mutable references to PubNubClientInstance are not allowed") + } +} + impl Clone for PubNubClientInstance { fn clone(&self) -> Self { Self { @@ -218,7 +228,7 @@ impl Clone for PubNubClientInstance { /// It's wrapped in `Arc` by [`PubNubClient`] and uses interior mutability for its internal state. /// /// Not intended to be used directly. Use [`PubNubClient`] instead. -#[derive(Debug, Builder)] +#[derive(Builder, Debug)] #[builder( pattern = "owned", name = "PubNubClientConfigBuilder", @@ -230,6 +240,14 @@ pub struct PubNubClientRef { /// Transport layer pub(crate) transport: T, + /// Data cryptor / decryptor + #[builder( + setter(custom, strip_option), + field(vis = "pub(crate)"), + default = "None" + )] + pub(crate) cryptor: Option>, + /// Instance ID #[builder( setter(into), @@ -250,7 +268,12 @@ pub struct PubNubClientRef { field(vis = "pub(crate)"), default = "Arc::new(spin::RwLock::new(String::new()))" )] - pub(crate) auth_token: Arc>, + pub(crate) auth_token: Arc>, + + /// Subscription module configuration + #[cfg(feature = "subscribe")] + #[builder(setter(skip), field(vis = "pub(crate)"))] + pub(crate) subscription: Arc>>, } impl PubNubClientInstance { @@ -275,7 +298,7 @@ impl PubNubClientInstance { /// # } /// # } /// - /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// # fn main() -> Result<(), PubNubError> { /// // note that MyTransport must implement the `Transport` trait /// let transport = MyTransport::new(); /// @@ -294,7 +317,7 @@ impl PubNubClientInstance { #[cfg(feature = "blocking")] pub fn with_blocking_transport(transport: T) -> PubNubClientBuilder where - T: crate::core::blocking::Transport, + T: crate::core::blocking::Transport + Send + Sync, { PubNubClientBuilder { transport: Some(transport), @@ -379,6 +402,39 @@ impl PubNubClientConfigBuilder { self } + /// Requests retry policy. + /// + /// The retry policy 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 the + /// [`PubNubClientConfigBuilder`]. + pub fn with_retry_policy(mut self, policy: RequestRetryPolicy) -> Self { + if let Some(configuration) = self.config.as_mut() { + configuration.retry_policy = policy; + } + + self + } + + /// Data encryption / decryption + /// + /// Cryptor used by client when publish messages / signals and receive them as real-time updates + /// from subscription module. + /// + /// It returns [`PubNubClientConfigBuilder`] that you can use to set the + /// configuration for the client. This is a part the + /// [`PubNubClientConfigBuilder`]. + pub fn with_cryptor(mut self, cryptor: C) -> Self + where + C: Cryptor + Send + Sync + 'static, + { + self.cryptor = Some(Some(Arc::new(cryptor))); + + self + } + /// Build a [`PubNubClient`] from the builder pub fn build(self) -> Result>, PubNubError> { self.build_internal() @@ -386,7 +442,7 @@ impl PubNubClientConfigBuilder { details: err.to_string(), }) .and_then(|pre_build| { - let token = Arc::new(spin::RwLock::new(String::new())); + let token = Arc::new(RwLock::new(String::new())); info!("Client Configuration: \n publish_key: {:?}\n subscribe_key: {}\n user_id: {}\n instance_id: {:?}", pre_build.config.publish_key, pre_build.config.subscribe_key, pre_build.config.user_id, pre_build.instance_id); Ok(PubNubClientRef { transport: PubNubMiddleware { @@ -401,10 +457,16 @@ impl PubNubClientConfigBuilder { next_seqn: pre_build.next_seqn, auth_token: token, config: pre_build.config, + cryptor: pre_build.cryptor.clone(), + + #[cfg(feature = "subscribe")] + subscription: Arc::new(RwLock::new(None)), }) }) - .map(|client| PubNubClientInstance { - inner: Arc::new(client), + .map(|client| { + PubNubClientInstance { + inner: Arc::new(client), + } }) } } @@ -429,6 +491,9 @@ pub struct PubNubConfig { /// Authorization key pub(crate) auth_key: Option>, + + /// Request retry policy + pub(crate) retry_policy: RequestRetryPolicy, } impl PubNubConfig { @@ -541,7 +606,7 @@ impl PubNubClientBuilder { /// # } /// # } /// - /// # fn main() -> Result<(), pubnub::core::PubNubError> { + /// # fn main() -> Result<(), PubNubError> { /// // note that MyTransport must implement the `Transport` trait /// let transport = MyTransport::new(); /// @@ -559,7 +624,7 @@ impl PubNubClientBuilder { #[cfg(feature = "blocking")] pub fn with_blocking_transport(transport: T) -> PubNubClientBuilder where - T: crate::core::blocking::Transport, + T: crate::core::blocking::Transport + Send + Sync, { PubNubClientBuilder { transport: Some(transport), @@ -659,6 +724,7 @@ where secret_key, user_id: Arc::new(user_id.into()), auth_key: None, + retry_policy: Default::default(), }), ..Default::default() } @@ -719,7 +785,7 @@ mod should { type_name::() } - let client = PubNubClientBuilder::with_transport(MockTransport::default()) + let client = PubNubClientBuilder::with_transport(MockTransport) .with_keyset(Keyset { subscribe_key: "", publish_key: Some(""), @@ -743,6 +809,7 @@ mod should { secret_key: Some("sec_key".into()), user_id: Arc::new("".into()), auth_key: None, + retry_policy: 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 new file mode 100644 index 00000000..1e8cb3b6 --- /dev/null +++ b/src/dx/subscribe/builders/mod.rs @@ -0,0 +1,32 @@ +//! Subscribe builders module. + +use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; + +#[doc(inline)] +pub(crate) use subscribe::SubscribeRequestBuilder; +pub(crate) mod subscribe; + +#[cfg(not(feature = "serde"))] +#[doc(inline)] +pub(crate) use subscription::SubscriptionWithDeserializerBuilder; + +#[doc(inline)] +pub use subscription::{SubscriptionBuilder, SubscriptionBuilderError}; +#[allow(missing_docs)] +pub mod subscription; + +/// Validate [`PubNubClientInstance`] configuration. +/// +/// Check whether if the [`PubNubConfig`] contains all the required fields set +/// for subscribe / unsubscribe endpoint usage or not. +pub(in crate::dx::subscribe::builders) fn validate_configuration( + client: &Option>, +) -> Result<(), String> { + if let Some(client) = client { + if client.config.subscribe_key.is_empty() { + return Err("Incomplete PubNub client configuration: 'subscribe_key' is empty.".into()); + } + } + + Ok(()) +} diff --git a/src/dx/subscribe/builders/subscribe.rs b/src/dx/subscribe/builders/subscribe.rs new file mode 100644 index 00000000..944483b9 --- /dev/null +++ b/src/dx/subscribe/builders/subscribe.rs @@ -0,0 +1,282 @@ +//! # PubNub subscribe module. +//! +//! This module has all the builders for subscription to real-time updates from +//! a list of channels and channel groups. + +use crate::dx::subscribe::SubscribeResponseBody; +use crate::{ + core::{ + utils::encoding::join_url_encoded, + Deserializer, PubNubError, Transport, {TransportMethod, TransportRequest}, + }, + dx::{ + pubnub_client::PubNubClientInstance, + subscribe::{builders, cancel::CancellationTask, result::SubscribeResult, SubscribeCursor}, + }, + lib::{ + alloc::{ + boxed::Box, + format, + string::{String, ToString}, + sync::Arc, + vec::Vec, + }, + collections::HashMap, + }, +}; +use derive_builder::Builder; +use futures::future::BoxFuture; +use futures::{select_biased, FutureExt}; + +/// The [`SubscribeRequestBuilder`] is used to build subscribe request which +/// will be used for real-time updates notification from the [`PubNub`] network. +/// +/// This struct used by the [`subscribe`] method of the [`PubNubClient`]. +/// The [`subscribe`] method is used to subscribe and receive real-time updates +/// from the [`PubNub`] network. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Builder)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::subscribe)", validate = "Self::validate"), + no_std +)] +#[allow(dead_code, missing_docs)] +pub(crate) struct SubscribeRequest +where + T: Transport + Send, +{ + /// Current client which can provide transportation to perform the request. + #[builder(field(vis = "pub(in crate::dx::subscribe)"), setter(custom))] + pub(in crate::dx::subscribe) pubnub_client: PubNubClientInstance, + + /// Channels from which real-time updates should be received. + /// + /// List of channels on which [`PubNubClient`] will subscribe and notify + /// about received real-time updates. + #[builder(field(vis = "pub(in crate::dx::subscribe)"), default = "Vec::new()")] + pub(in crate::dx::subscribe) channels: Vec, + + /// 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. + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(strip_option), + default = "Vec::new()" + )] + pub(in crate::dx::subscribe) channel_groups: Vec, + + /// 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: SubscribeCursor, + + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(skip), + default = "300" + )] + pub(in crate::dx::subscribe) heartbeat: u32, + + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(strip_option), + default = "None" + )] + pub(in crate::dx::subscribe) filter_expression: Option, +} + +impl SubscribeRequest +where + T: Transport, +{ + /// 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; + let channels = join_url_encoded( + self.channels + .iter() + .map(|v| v.as_str()) + .collect::>() + .as_slice(), + ",", + ) + .unwrap_or(",".into()); + let mut query: HashMap = HashMap::new(); + query.extend::>(self.cursor.clone().into()); + + // Serialize list of channel groups and add into query parameters list. + join_url_encoded( + self.channel_groups + .iter() + .map(|v| v.as_str()) + .collect::>() + .as_slice(), + ",", + ) + .filter(|string| !string.is_empty()) + .and_then(|channel_groups| query.insert("channel-group".into(), channel_groups)); + + self.filter_expression + .as_ref() + .filter(|e| !e.is_empty()) + .and_then(|e| query.insert("filter-expr".into(), e.into())); + + TransportRequest { + path: format!("/v2/subscribe/{sub_key}/{channels}/0"), + query_parameters: query, + method: TransportMethod::Get, + ..Default::default() + } + } +} + +impl SubscribeRequestBuilder +where + T: Transport, +{ + /// 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 groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else { + Ok(()) + } + }) + } +} + +impl SubscribeRequestBuilder +where + T: Transport, +{ + /// Build and call request. + pub async fn execute( + self, + deserializer: Arc, + delay: Arc, + cancel_task: CancellationTask, + ) -> Result + where + D: Deserializer + ?Sized, + F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, + { + // Build request instance and report errors if any. + let request = self + .build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; + + let transport_request = request.transport_request(); + let client = request.pubnub_client.clone(); + + select_biased! { + _ = cancel_task.wait_for_cancel().fuse() => { + Err(PubNubError::EffectCanceled) + }, + response = async { + // Postpone request execution if required. + delay().await; + + // Request configured endpoint. + let response = client.transport.send(transport_request).await?; + response + .clone() + .body + .map(|bytes| { + let deserialize_result = deserializer.deserialize(&bytes); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())) + )) + } else { + deserialize_result + } + }) + .map_or( + Err(PubNubError::general_api_error( + "No body in the response!", + None, + Some(Box::new(response.clone())) + )), + |response_body| { + response_body.and_then::(|body| { + body.try_into().map_err(|response_error: PubNubError| { + response_error.attach_response(response) + }) + }) + }, + ) + }.fuse() => { + response + } + } + } +} + +#[cfg(test)] +mod should { + use super::*; + use crate::{ + core::TransportResponse, providers::deserialization_serde::SerdeDeserializer, + PubNubClientBuilder, + }; + use futures::future::ready; + + #[tokio::test] + async fn be_able_to_cancel_subscribe_call() { + 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 (tx, rx) = async_channel::bounded(1); + + let cancel_task = CancellationTask::new(rx, "test".into()); + + tx.send("test".into()).await.unwrap(); + + let result = PubNubClientBuilder::with_transport(MockTransport) + .with_keyset(crate::Keyset { + subscribe_key: "test", + publish_key: Some("test"), + secret_key: None, + }) + .with_user_id("test") + .build() + .unwrap() + .subscribe_request() + .channels(vec!["test".into()]) + .execute( + Arc::new(SerdeDeserializer), + Arc::new(|| ready(()).boxed()), + cancel_task, + ) + .await; + + assert!(matches!(result, Err(PubNubError::EffectCanceled))); + } +} diff --git a/src/dx/subscribe/builders/subscription.rs b/src/dx/subscribe/builders/subscription.rs new file mode 100644 index 00000000..2899a259 --- /dev/null +++ b/src/dx/subscribe/builders/subscription.rs @@ -0,0 +1,561 @@ +use crate::{ + core::{Deserializer, PubNubError}, + dx::subscribe::{ + result::Update, types::SubscribeStreamEvent, SubscribeResponseBody, SubscribeStatus, + SubscriptionConfiguration, + }, + lib::{ + alloc::{ + collections::VecDeque, + string::{String, ToString}, + sync::Arc, + vec::Vec, + }, + core::{ + fmt::{Debug, Formatter}, + ops::{Deref, DerefMut}, + pin::Pin, + task::{Context, Poll, Waker}, + }, + }, +}; +use derive_builder::Builder; +use futures::Stream; +use spin::RwLock; +use uuid::Uuid; + +/// 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>, +} + +/// 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 +)] +#[allow(dead_code)] +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>>, + + /// Channels from which real-time updates should be received. + /// + /// List of channels on which [`PubNubClient`] will subscribe and notify + /// about received real-time updates. + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(into, strip_option), + default = "Vec::new()" + )] + pub(in crate::dx::subscribe) channels: Vec, + + /// 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. + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(into, strip_option), + default = "Vec::new()" + )] + pub(in crate::dx::subscribe) channel_groups: Vec, + + /// 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, + + #[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)"), + 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>, + + /// Subscription stream waker. + /// + /// Handler used each time when new data available for a stream listener. + #[builder( + field(vis = "pub(in crate::dx::subscribe)"), + setter(custom), + default = "RwLock::new(None)" + )] + waker: 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>>, +} + +/// [`SubscriptionWithDeserializerBuilder`] is used to configure a subscription +/// listener with a custom deserializer. +pub struct SubscriptionWithDeserializerBuilder { + /// Subscription module configuration. + pub(in crate::dx::subscribe) subscription: Arc>>, +} + +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 groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else { + Ok(()) + } + } +} + +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.subscription_manager.register(subscription.clone()) + } + subscription + }) + .map_err(|e| PubNubError::SubscribeInitialization { + details: e.to_string(), + }) + } +} + +impl SubscriptionWithDeserializerBuilder { + /// Add custom deserializer. + /// + /// Adds the deserializer to the [`SubscriptionBuilder`]. + /// + /// Instance of [`SubscriptionBuilder`] returned. + pub fn deserialize_with(self, deserializer: D) -> SubscriptionBuilder + where + D: Deserializer + 'static, + { + { + if let Some(subscription) = self.subscription.write().as_mut() { + subscription + .deserializer + .is_none() + .then(|| subscription.deserializer = Some(Arc::new(deserializer))); + } + } + + SubscriptionBuilder { + subscription: Some(self.subscription), + ..Default::default() + } + } +} + +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.channels, self.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.subscription_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.into_iter()); + 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 { + self.channels.contains(&update.channel()) + || self.channel_groups.contains(&update.channel_group()) + } +} + +impl SubscriptionStream { + fn new(updates: VecDeque) -> Self { + let mut stream_updates = VecDeque::with_capacity(100); + stream_updates.extend(updates.into_iter()); + + Self { + inner: Arc::new(SubscriptionStreamRef { + updates: RwLock::new(stream_updates), + waker: RwLock::new(None), + }), + } + } + + 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> { + 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/cancel.rs b/src/dx/subscribe/cancel.rs new file mode 100644 index 00000000..c94d516e --- /dev/null +++ b/src/dx/subscribe/cancel.rs @@ -0,0 +1,56 @@ +use async_channel::Receiver; + +use crate::{ + core::PubNubError, + lib::alloc::{format, string::String}, +}; + +#[derive(Debug)] +pub(crate) struct CancellationTask { + cancel_rx: Receiver, + id: String, +} + +impl CancellationTask { + pub(super) fn new(cancel_rx: Receiver, id: String) -> Self { + Self { cancel_rx, id } + } + + pub async fn wait_for_cancel(&self) -> Result<(), PubNubError> { + loop { + if self + .cancel_rx + .recv() + .await + .map_err(|err| PubNubError::Transport { + details: format!("Cancellation pipe failed: {err}"), + response: None, + })? + .eq(&self.id) + { + break; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod should { + use super::*; + + #[tokio::test] + async fn wait_for_cancel() { + let (cancel_tx, cancel_rx) = async_channel::bounded(2); + + let cancel_task = CancellationTask::new(cancel_rx, "id".into()); + + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + cancel_tx.send("id".into()).await.unwrap(); + }); + + cancel_task.wait_for_cancel().await.unwrap(); + } +} diff --git a/src/dx/subscribe/event_engine/effect_handler.rs b/src/dx/subscribe/event_engine/effect_handler.rs index 8758ab5b..d209312e 100644 --- a/src/dx/subscribe/event_engine/effect_handler.rs +++ b/src/dx/subscribe/event_engine/effect_handler.rs @@ -1,68 +1,56 @@ +use async_channel::Sender; + +use crate::core::RequestRetryPolicy; use crate::{ - core::{event_engine::EffectHandler, PubNubError}, - dx::subscribe::{ - event_engine::{SubscribeEffect, SubscribeEffectInvocation}, - SubscribeCursor, SubscribeStatus, + core::event_engine::EffectHandler, + dx::subscribe::event_engine::{ + effects::{EmitMessagesEffectExecutor, EmitStatusEffectExecutor, SubscribeEffectExecutor}, + SubscribeEffect, SubscribeEffectInvocation, + }, + lib::{ + alloc::{string::String, sync::Arc}, + core::fmt::{Debug, Formatter, Result}, }, - lib::alloc::{string::String, vec::Vec}, }; -use super::SubscribeEvent; - -pub(crate) type HandshakeFunction = fn( - channels: &Option>, - channel_groups: &Option>, - attempt: u8, - reason: Option, -) -> Result, PubNubError>; - -pub(crate) type ReceiveFunction = fn( - channels: &Option>, - channel_groups: &Option>, - cursor: &SubscribeCursor, - attempt: u8, - reason: Option, -) -> Result, PubNubError>; - -pub(crate) type EmitFunction = fn(data: EmitData) -> Result<(), PubNubError>; - -/// Data emitted by subscription. -/// -/// This data is emitted by subscription and is used to create subscription -/// events. -pub(crate) enum EmitData { - /// Status emitted by subscription. - SubscribeStatus(SubscribeStatus), - - /// Messages emitted by subscription. - /// TODO: Replace String with Message type - Messages(Vec), -} - /// Subscription effect handler. /// /// Handler responsible for effects implementation and creation in response on /// effect invocation. #[allow(dead_code)] pub(crate) struct SubscribeEffectHandler { - /// Handshake function pointer. - handshake: HandshakeFunction, + /// Subscribe call function pointer. + subscribe_call: Arc, + + /// Emit status function pointer. + emit_status: Arc, + + /// Emit messages function pointer. + emit_messages: Arc, - /// Receive updates function pointer. - receive: ReceiveFunction, + /// Retry policy. + retry_policy: RequestRetryPolicy, - /// Emit data function pointer. - emit: EmitFunction, + /// Cancellation channel. + cancellation_channel: Sender, } impl SubscribeEffectHandler { /// Create subscribe event handler. #[allow(dead_code)] - pub fn new(handshake: HandshakeFunction, receive: ReceiveFunction, emit: EmitFunction) -> Self { + pub fn new( + subscribe_call: Arc, + emit_status: Arc, + emit_messages: Arc, + retry_policy: RequestRetryPolicy, + cancellation_channel: Sender, + ) -> Self { SubscribeEffectHandler { - handshake, - receive, - emit, + subscribe_call, + emit_status, + emit_messages, + retry_policy, + cancellation_channel, } } } @@ -76,7 +64,8 @@ impl EffectHandler for SubscribeEffe } => Some(SubscribeEffect::Handshake { channels: channels.clone(), channel_groups: channel_groups.clone(), - executor: self.handshake, + executor: self.subscribe_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), }), SubscribeEffectInvocation::HandshakeReconnect { channels, @@ -88,7 +77,9 @@ impl EffectHandler for SubscribeEffe channel_groups: channel_groups.clone(), attempts: *attempts, reason: reason.clone(), - executor: self.handshake, + retry_policy: self.retry_policy.clone(), + executor: self.subscribe_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), }), SubscribeEffectInvocation::Receive { channels, @@ -97,8 +88,9 @@ impl EffectHandler for SubscribeEffe } => Some(SubscribeEffect::Receive { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, - executor: self.receive, + cursor: cursor.clone(), + executor: self.subscribe_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), }), SubscribeEffectInvocation::ReceiveReconnect { channels, @@ -109,26 +101,30 @@ impl EffectHandler for SubscribeEffe } => Some(SubscribeEffect::ReceiveReconnect { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), attempts: *attempts, reason: reason.clone(), - executor: self.receive, + retry_policy: self.retry_policy.clone(), + executor: self.subscribe_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), + }), + SubscribeEffectInvocation::EmitStatus(status) => Some(SubscribeEffect::EmitStatus { + status: status.clone(), + executor: self.emit_status.clone(), }), - SubscribeEffectInvocation::EmitStatus(status) => { - // TODO: Provide emit status effect - Some(SubscribeEffect::EmitStatus { - status: *status, - executor: self.emit, - }) - } SubscribeEffectInvocation::EmitMessages(messages) => { - // TODO: Provide emit messages effect Some(SubscribeEffect::EmitMessages { - messages: messages.clone(), - executor: self.emit, + updates: messages.clone(), + executor: self.emit_messages.clone(), }) } _ => None, } } } + +impl Debug for SubscribeEffectHandler { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "SubscribeEffectHandler {{}}") + } +} diff --git a/src/dx/subscribe/event_engine/effects/emit_messages.rs b/src/dx/subscribe/event_engine/effects/emit_messages.rs index 47922dc6..8ab16b8b 100644 --- a/src/dx/subscribe/event_engine/effects/emit_messages.rs +++ b/src/dx/subscribe/event_engine/effects/emit_messages.rs @@ -1,41 +1,54 @@ -use crate::dx::subscribe::event_engine::effect_handler::EmitData; -use crate::dx::subscribe::event_engine::{effect_handler::EmitFunction, SubscribeEvent}; -use crate::lib::alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; - -pub(super) fn execute(messages: &[String], executor: EmitFunction) -> Option> { - // TODO: is this clone needed? - executor(EmitData::Messages(messages.to_owned())) - .map(|_| vec![]) - .ok() +use crate::{ + dx::subscribe::{ + event_engine::{effects::EmitMessagesEffectExecutor, SubscribeEvent}, + result::Update, + }, + lib::alloc::{sync::Arc, vec, vec::Vec}, +}; +use log::info; + +pub(super) async fn execute( + updates: Vec, + executor: &Arc, +) -> Vec { + info!("Emit updates: {updates:?}"); + + executor(updates); + + vec![] } #[cfg(test)] mod should { use super::*; - use crate::core::PubNubError; - - #[test] - fn emit_status() { - fn mock_handshake_function(data: EmitData) -> Result<(), PubNubError> { - assert!(matches!(data, EmitData::Messages(_))); - - Ok(()) - } - - let result = execute(&[], mock_handshake_function); - - assert!(result.is_some()); - assert!(result.unwrap().is_empty()) - } - - #[test] - fn return_emit_failure_event_on_err() { - fn mock_handshake_function(_data: EmitData) -> Result<(), PubNubError> { - Err(PubNubError::Transport { - details: "test".into(), - }) - } - - assert!(execute(&[], mock_handshake_function).is_none()); + use crate::dx::subscribe::types::Message; + + #[tokio::test] + async fn emit_expected_status() { + let message = Message { + sender: Some("test-user".into()), + timestamp: 1234567890, + channel: "test".to_string(), + subscription: "test-group".to_string(), + data: vec![], + r#type: None, + space_id: None, + decryption_error: None, + }; + + let emit_message_function: Arc = Arc::new(|updates| { + let emitted_update = updates.first().expect("update should be passed"); + assert!(matches!(emitted_update, Update::Message(_))); + + if let Update::Message(message) = emitted_update { + assert_eq!(*message, message.clone()); + } + }); + + execute( + vec![Update::Message(message.clone())], + &emit_message_function, + ) + .await; } } diff --git a/src/dx/subscribe/event_engine/effects/emit_status.rs b/src/dx/subscribe/event_engine/effects/emit_status.rs index 96016c6f..a5b85ca9 100644 --- a/src/dx/subscribe/event_engine/effects/emit_status.rs +++ b/src/dx/subscribe/event_engine/effects/emit_status.rs @@ -1,44 +1,33 @@ -use crate::dx::subscribe::event_engine::effect_handler::EmitData; -use crate::dx::subscribe::event_engine::{effect_handler::EmitFunction, SubscribeEvent}; -use crate::dx::subscribe::SubscribeStatus; -use crate::lib::alloc::{vec, vec::Vec}; - -pub(super) fn execute( +use crate::{ + dx::subscribe::{ + event_engine::{effects::EmitStatusEffectExecutor, SubscribeEvent}, + SubscribeStatus, + }, + lib::alloc::{sync::Arc, vec, vec::Vec}, +}; +use log::info; + +pub(super) async fn execute( status: SubscribeStatus, - executor: EmitFunction, -) -> Option> { - executor(EmitData::SubscribeStatus(status)) - .map(|_| vec![]) - .ok() + executor: &Arc, +) -> Vec { + info!("Emit status: {status:?}"); + + executor(status); + + vec![] } #[cfg(test)] mod should { use super::*; - use crate::core::PubNubError; - - #[test] - fn emit_status() { - fn mock_handshake_function(data: EmitData) -> Result<(), PubNubError> { - assert!(matches!(data, EmitData::SubscribeStatus(_))); - - Ok(()) - } - - let result = execute(SubscribeStatus::Connected, mock_handshake_function); - - assert!(result.is_some()); - assert!(result.unwrap().is_empty()) - } - #[test] - fn return_emit_failure_event_on_err() { - fn mock_handshake_function(_data: EmitData) -> Result<(), PubNubError> { - Err(PubNubError::Transport { - details: "test".into(), - }) - } + #[tokio::test] + async fn emit_expected_status() { + let emit_status_function: Arc = Arc::new(|status| { + assert!(matches!(status, SubscribeStatus::Connected)); + }); - assert!(execute(SubscribeStatus::Connected, mock_handshake_function).is_none()); + execute(SubscribeStatus::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 997126d3..077e9be3 100644 --- a/src/dx/subscribe/event_engine/effects/handshake.rs +++ b/src/dx/subscribe/event_engine/effects/handshake.rs @@ -1,77 +1,111 @@ -use crate::dx::subscribe::event_engine::{effect_handler::HandshakeFunction, SubscribeEvent}; -use crate::lib::alloc::{string::String, vec, vec::Vec}; +use crate::{ + dx::subscribe::{ + event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, + SubscriptionParams, + }, + lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, +}; +use futures::TryFutureExt; +use log::info; -pub(super) fn execute( +pub(super) async fn execute( channels: &Option>, channel_groups: &Option>, - executor: HandshakeFunction, -) -> Option> { - Some( - executor(channels, channel_groups, 0, None) - .unwrap_or_else(|err| vec![SubscribeEvent::HandshakeFailure { reason: err }]), + effect_id: &str, + executor: &Arc, +) -> Vec { + info!( + "Handshake for\nchannels: {:?}\nchannel groups: {:?}", + channels.as_ref().unwrap_or(&Vec::new()), + channel_groups.as_ref().unwrap_or(&Vec::new()) + ); + + executor(SubscriptionParams { + channels, + channel_groups, + cursor: None, + attempt: 0, + reason: None, + effect_id, + }) + .map_ok_or_else( + |error| { + log::error!("Handshake error: {:?}", error); + vec![SubscribeEvent::HandshakeFailure { reason: error }] + }, + |subscribe_result| { + vec![SubscribeEvent::HandshakeSuccess { + cursor: subscribe_result.cursor, + }] + }, ) + .await } #[cfg(test)] mod should { use super::*; - use crate::{core::PubNubError, dx::subscribe::SubscribeCursor}; + use crate::{core::PubNubError, dx::subscribe::SubscribeResult}; + use futures::FutureExt; - #[test] - fn initialize_handshake_for_first_attempt() { - fn mock_handshake_function( - channels: &Option>, - channel_groups: &Option>, - attempt: u8, - reason: Option, - ) -> Result, PubNubError> { - assert_eq!(channels, &Some(vec!["ch1".to_string()])); - assert_eq!(channel_groups, &Some(vec!["cg1".to_string()])); - assert_eq!(attempt, 0); - assert_eq!(reason, None); + #[tokio::test] + async fn initialize_handshake_for_first_attempt() { + let mock_handshake_function: Arc = Arc::new(move |params| { + assert_eq!(params.channels, &Some(vec!["ch1".to_string()])); + assert_eq!(params.channel_groups, &Some(vec!["cg1".to_string()])); + assert_eq!(params.attempt, 0); + assert_eq!(params.cursor, None); + assert_eq!(params.reason, None); + assert_eq!(params.effect_id, "id"); - Ok(vec![SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { - timetoken: 0, - region: 0, - }, - }]) - } + async move { + Ok(SubscribeResult { + cursor: Default::default(), + messages: vec![], + }) + } + .boxed() + }); let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), - mock_handshake_function, - ); + "id", + &mock_handshake_function, + ) + .await; - assert!(result.is_some()); + assert!(!result.is_empty()); assert!(matches!( - result.unwrap().first().unwrap(), - &SubscribeEvent::HandshakeSuccess { .. } - )) + result.first().unwrap(), + SubscribeEvent::HandshakeSuccess { .. } + )); } - #[test] - fn return_handskahe_failure_event_on_err() { - fn mock_handshake_function( - _channels: &Option>, - _channel_groups: &Option>, - _attempt: u8, - _reason: Option, - ) -> Result, PubNubError> { - Err(PubNubError::Transport { - details: "test".into(), - }) - } + #[tokio::test] + async fn return_handshake_failure_event_on_err() { + let mock_handshake_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: None, + }) + } + .boxed() + }); - let binding = execute( + let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), - mock_handshake_function, + "id", + &mock_handshake_function, ) - .unwrap(); - let result = &binding[0]; + .await; - assert!(matches!(result, &SubscribeEvent::HandshakeFailure { .. })); + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + SubscribeEvent::HandshakeFailure { .. } + )); } } diff --git a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs index 3acc06e7..d7a5130d 100644 --- a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs @@ -1,52 +1,88 @@ -use crate::lib::alloc::{string::String, vec, vec::Vec}; use crate::{ - core::PubNubError, - dx::subscribe::event_engine::{effect_handler::HandshakeFunction, SubscribeEvent}, + core::{PubNubError, RequestRetryPolicy}, + dx::subscribe::{ + event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, + SubscriptionParams, + }, + lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; +use futures::TryFutureExt; +use log::info; -pub(super) fn execute( +pub(super) async fn execute( channels: &Option>, channel_groups: &Option>, attempt: u8, reason: PubNubError, - executor: HandshakeFunction, -) -> Option> { - Some( - executor(channels, channel_groups, attempt, Some(reason)) - .unwrap_or_else(|err| vec![SubscribeEvent::HandshakeReconnectFailure { reason: err }]), + effect_id: &str, + retry_policy: &RequestRetryPolicy, + executor: &Arc, +) -> Vec { + if !retry_policy.retriable(&attempt, Some(&reason)) { + return vec![SubscribeEvent::HandshakeReconnectGiveUp { reason }]; + } + + info!( + "Handshake reconnection for\nchannels: {:?}\nchannel groups: {:?}", + channels.as_ref().unwrap_or(&Vec::new()), + channel_groups.as_ref().unwrap_or(&Vec::new()), + ); + + executor(SubscriptionParams { + channels, + channel_groups, + cursor: None, + attempt, + reason: Some(reason), + effect_id, + }) + .map_ok_or_else( + |error| { + log::error!("Handshake reconnection error: {:?}", error); + + (!matches!(error, PubNubError::EffectCanceled)) + .then(|| vec![SubscribeEvent::HandshakeReconnectFailure { reason: error }]) + .unwrap_or(vec![]) + }, + |subscribe_result| { + vec![SubscribeEvent::HandshakeReconnectSuccess { + cursor: subscribe_result.cursor, + }] + }, ) + .await } #[cfg(test)] mod should { use super::*; - use crate::{core::PubNubError, dx::subscribe::SubscribeCursor}; + use crate::{core::PubNubError, dx::subscribe::result::SubscribeResult}; + use futures::FutureExt; - #[test] - fn initialize_handshake_reconnect_attempt() { - fn mock_handshake_function( - channels: &Option>, - channel_groups: &Option>, - attempt: u8, - reason: Option, - ) -> Result, PubNubError> { - assert_eq!(channels, &Some(vec!["ch1".to_string()])); - assert_eq!(channel_groups, &Some(vec!["cg1".to_string()])); - assert_eq!(attempt, 1); + #[tokio::test] + async fn initialize_handshake_reconnect_attempt() { + let mock_handshake_function: Arc = Arc::new(move |params| { + assert_eq!(params.channels, &Some(vec!["ch1".to_string()])); + assert_eq!(params.channel_groups, &Some(vec!["cg1".to_string()])); + assert_eq!(params.attempt, 1); + assert_eq!(params.cursor, None); assert_eq!( - reason.unwrap(), + params.reason.unwrap(), PubNubError::Transport { details: "test".into(), + response: None } ); + assert_eq!(params.effect_id, "id"); - Ok(vec![SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { - timetoken: 0, - region: 0, - }, - }]) - } + async move { + Ok(SubscribeResult { + cursor: Default::default(), + messages: vec![], + }) + } + .boxed() + }); let result = execute( &Some(vec!["ch1".to_string()]), @@ -54,42 +90,54 @@ mod should { 1, PubNubError::Transport { details: "test".into(), + response: None, + }, + "id", + &RequestRetryPolicy::Linear { + delay: 0, + max_retry: 1, }, - mock_handshake_function, - ); + &mock_handshake_function, + ) + .await; - assert!(result.is_some()); - assert!(!result.unwrap().is_empty()) + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + SubscribeEvent::HandshakeReconnectSuccess { .. } + )); } - #[test] - fn return_handskahe_failure_event_on_err() { - fn mock_handshake_function( - _channels: &Option>, - _channel_groups: &Option>, - _attempt: u8, - _reason: Option, - ) -> Result, PubNubError> { - Err(PubNubError::Transport { - details: "test".into(), - }) - } + #[tokio::test] + async fn return_handshake_reconnect_failure_event_on_err() { + let mock_handshake_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: None, + }) + } + .boxed() + }); - let binding = execute( + let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), 1, PubNubError::Transport { details: "test".into(), + response: None, }, - mock_handshake_function, + "id", + &RequestRetryPolicy::None, + &mock_handshake_function, ) - .unwrap(); - let result = &binding[0]; + .await; + assert!(!result.is_empty()); assert!(matches!( - result, - &SubscribeEvent::HandshakeReconnectFailure { .. } + result.first().unwrap(), + SubscribeEvent::HandshakeReconnectGiveUp { .. } )); } } diff --git a/src/dx/subscribe/event_engine/effects/mod.rs b/src/dx/subscribe/event_engine/effects/mod.rs index 76ce0e89..768b6444 100644 --- a/src/dx/subscribe/event_engine/effects/mod.rs +++ b/src/dx/subscribe/event_engine/effects/mod.rs @@ -1,12 +1,17 @@ -use crate::dx::subscribe::event_engine::{SubscribeEffectInvocation, SubscribeEvent}; use crate::{ - core::{event_engine::Effect, PubNubError}, - dx::subscribe::{SubscribeCursor, SubscribeStatus}, - lib::alloc::{string::String, vec::Vec}, + core::{event_engine::Effect, PubNubError, RequestRetryPolicy}, + dx::subscribe::{ + event_engine::{SubscribeEffectInvocation, SubscribeEvent}, + result::{SubscribeResult, Update}, + SubscribeCursor, SubscribeStatus, SubscriptionParams, + }, + lib::{ + alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}, + core::fmt::{Debug, Formatter}, + }, }; - -use super::effect_handler::EmitFunction; -use super::{HandshakeFunction, ReceiveFunction}; +use async_channel::Sender; +use futures::future::BoxFuture; mod emit_messages; mod emit_status; @@ -15,6 +20,14 @@ mod handshake_reconnection; mod receive; mod receive_reconnection; +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; + +// TODO: maybe move executor and cancellation_channel to super struct? /// Subscription state machine effects. #[allow(dead_code)] pub(crate) enum SubscribeEffect { @@ -35,7 +48,12 @@ pub(crate) enum SubscribeEffect { /// Executor function. /// /// Function which will be used to execute initial subscription. - executor: HandshakeFunction, + executor: Arc, + + /// Cancellation channel. + /// + /// Channel which will be used to cancel effect execution. + cancellation_channel: Sender, }, /// Retry initial subscribe effect invocation. @@ -60,10 +78,18 @@ pub(crate) enum SubscribeEffect { /// Initial subscribe attempt failure reason. reason: PubNubError, + /// Retry policy. + retry_policy: RequestRetryPolicy, + /// Executor function. /// /// Function which will be used to execute initial subscription. - executor: HandshakeFunction, + executor: Arc, + + /// Cancellation channel. + /// + /// Channel which will be used to cancel effect execution. + cancellation_channel: Sender, }, /// Receive updates effect invocation. @@ -88,7 +114,12 @@ pub(crate) enum SubscribeEffect { /// Executor function. /// /// Function which will be used to execute receive updates. - executor: ReceiveFunction, + executor: Arc, + + /// Cancellation channel. + /// + /// Channel which will be used to cancel effect execution. + cancellation_channel: Sender, }, /// Retry receive updates effect invocation. @@ -119,44 +150,105 @@ pub(crate) enum SubscribeEffect { /// Receive updates attempt failure reason. reason: PubNubError, + /// Retry policy. + retry_policy: RequestRetryPolicy, + /// Executor function. /// /// Function which will be used to execute receive updates. - executor: ReceiveFunction, + executor: Arc, + + /// Cancellation channel. + /// + /// Channel which will be used to cancel effect execution. + cancellation_channel: Sender, }, /// Status change notification effect invocation. EmitStatus { - /// Current subscription status. - /// - /// Used to notify about subscription status changes. + /// Status which should be emitted. status: SubscribeStatus, - /// Emiting function. + /// Executor function. /// - /// Function which will be used to emit subscription status changes. - executor: EmitFunction, + /// Function which will be used to execute receive updates. + executor: Arc, }, /// Received updates notification effect invocation. EmitMessages { - /// Received Messages - /// - /// Messages ready to be emitted to the user. - messages: Vec, + /// Updates which should be emitted. + updates: Vec, - /// Emiting function. + /// Executor function. /// - /// Function which will be used to emit subscription status changes. - executor: EmitFunction, + /// Function which will be used to execute receive updates. + executor: Arc, }, } +impl Debug for SubscribeEffect { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + SubscribeEffect::Handshake { + channels, + channel_groups, + .. + } => write!( + f, + "SubscribeEffect::Handshake {{ channels: {channels:?}, channel groups: \ + {channel_groups:?} }}" + ), + SubscribeEffect::HandshakeReconnect { + channels, + channel_groups, + attempts, + reason, + .. + } => write!( + f, + "SubscribeEffect::HandshakeReconnect {{ channels: {channels:?}, channel groups: \ + {channel_groups:?}, attempts: {attempts:?}, reason: {reason:?} }}" + ), + SubscribeEffect::Receive { + channels, + channel_groups, + cursor, + .. + } => write!( + f, + "SubscribeEffect::Receive {{ channels: {channels:?}, channel groups: \ + {channel_groups:?}, cursor: {cursor:?} }}" + ), + SubscribeEffect::ReceiveReconnect { + channels, + channel_groups, + attempts, + reason, + .. + } => write!( + f, + "SubscribeEffect::ReceiveReconnect {{ channels: {channels:?}, channel groups: \ + {channel_groups:?}, attempts: {attempts:?}, reason: {reason:?} }}" + ), + SubscribeEffect::EmitStatus { status, .. } => { + write!(f, "SubscribeEffect::EmitStatus {{ status: {status:?} }}") + } + SubscribeEffect::EmitMessages { updates, .. } => { + write!( + f, + "SubscribeEffect::EmitMessages {{ messages: {updates:?} }}" + ) + } + } + } +} + +#[async_trait::async_trait] impl Effect for SubscribeEffect { type Invocation = SubscribeEffectInvocation; fn id(&self) -> String { - // TODO: Identifiers need to be unique, so we won't cancel wrong effect match self { SubscribeEffect::Handshake { .. } => "HANDSHAKE_EFFECT".into(), SubscribeEffect::HandshakeReconnect { .. } => "HANDSHAKE_RECONNECT_EFFECT".into(), @@ -166,63 +258,124 @@ impl Effect for SubscribeEffect { SubscribeEffect::EmitMessages { .. } => "EMIT_MESSAGES_EFFECT".into(), } } - fn run(&self, mut f: F) - where - F: FnMut(Option>), - { - // TODO: Run actual effect implementation. Maybe Effect.run function need change something in arguments. - let events = match self { + + async fn run(&self) -> Vec { + match self { SubscribeEffect::Handshake { channels, channel_groups, executor, - } => handshake::execute(channels, channel_groups, *executor), + .. + } => handshake::execute(channels, channel_groups, &self.id(), executor).await, SubscribeEffect::HandshakeReconnect { channels, channel_groups, attempts, reason, + retry_policy, executor, - } => handshake_reconnection::execute( - channels, - channel_groups, - *attempts, - reason.clone(), // TODO: Does run function need to borrow self? Or we can consume it? - *executor, - ), + .. + } => { + handshake_reconnection::execute( + channels, + channel_groups, + *attempts, + reason.clone(), // TODO: Does run function need to borrow self? Or we can consume it? + &self.id(), + retry_policy, + executor, + ) + .await + } SubscribeEffect::Receive { channels, channel_groups, cursor, executor, - } => receive::execute(channels, channel_groups, cursor, *executor), + .. + } => receive::execute(channels, channel_groups, cursor, &self.id(), executor).await, SubscribeEffect::ReceiveReconnect { channels, channel_groups, cursor, attempts, reason, + retry_policy, executor, - } => receive_reconnection::execute( - channels, - channel_groups, - cursor, - *attempts, - reason.clone(), // TODO: Does run function need to borrow self? Or we can consume it? - *executor, - ), + .. + } => { + receive_reconnection::execute( + channels, + channel_groups, + cursor, + *attempts, + reason.clone(), // TODO: Does run function need to borrow self? Or we can consume it? + &self.id(), + retry_policy, + executor, + ) + .await + } SubscribeEffect::EmitStatus { status, executor } => { - emit_status::execute(*status, *executor) + emit_status::execute(status.clone(), executor).await } - SubscribeEffect::EmitMessages { messages, executor } => { - emit_messages::execute(messages, *executor) + SubscribeEffect::EmitMessages { updates, executor } => { + emit_messages::execute(updates.clone(), executor).await } - }; - - f(events); + } } fn cancel(&self) { - // TODO: Cancellation required for corresponding SubscribeEffect variants. + match self { + SubscribeEffect::Handshake { + cancellation_channel, + .. + } + | SubscribeEffect::HandshakeReconnect { + cancellation_channel, + .. + } + | SubscribeEffect::Receive { + cancellation_channel, + .. + } + | SubscribeEffect::ReceiveReconnect { + cancellation_channel, + .. + } => { + cancellation_channel + .send_blocking(self.id()) + .expect("cancellation pipe is broken!"); + } + _ => { /* cannot cancel other effects */ } + } + } +} + +#[cfg(test)] +mod should { + use super::*; + + #[tokio::test] + async fn send_cancelation_notification() { + let (tx, rx) = async_channel::bounded(1); + + let effect = SubscribeEffect::Handshake { + channels: None, + channel_groups: None, + executor: Arc::new(|_| { + Box::pin(async move { + Ok(SubscribeResult { + cursor: SubscribeCursor::default(), + messages: vec![], + }) + }) + }), + cancellation_channel: tx, + }; + + effect.cancel(); + + 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 d2e31635..18281f1e 100644 --- a/src/dx/subscribe/event_engine/effects/receive.rs +++ b/src/dx/subscribe/event_engine/effects/receive.rs @@ -1,98 +1,116 @@ -use crate::dx::subscribe::{ - event_engine::{ReceiveFunction, SubscribeEvent}, - SubscribeCursor, +use crate::{ + dx::subscribe::{ + event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, + SubscribeCursor, SubscriptionParams, + }, + lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; -use crate::lib::alloc::{string::String, vec, vec::Vec}; +use futures::TryFutureExt; +use log::info; -pub(crate) fn execute( +pub(crate) async fn execute( channels: &Option>, channel_groups: &Option>, cursor: &SubscribeCursor, - executor: ReceiveFunction, -) -> Option> { - Some( - executor(channels, channel_groups, cursor, 0, None) - .unwrap_or_else(|err| vec![SubscribeEvent::ReceiveFailure { reason: err }]), + effect_id: &str, + executor: &Arc, +) -> Vec { + info!( + "Receive at {:?} for\nchannels: {:?}\nchannel groups: {:?}", + cursor.timetoken, + channels.as_ref().unwrap_or(&Vec::new()), + channel_groups.as_ref().unwrap_or(&Vec::new()), + ); + + executor(SubscriptionParams { + channels, + channel_groups, + cursor: Some(cursor), + attempt: 0, + reason: None, + effect_id, + }) + .map_ok_or_else( + |error| { + log::error!("Receive error: {:?}", error); + vec![SubscribeEvent::ReceiveFailure { reason: error }] + }, + |subscribe_result| { + vec![SubscribeEvent::ReceiveSuccess { + cursor: subscribe_result.cursor, + messages: subscribe_result.messages, + }] + }, ) + .await } #[cfg(test)] mod should { use super::*; - use crate::{core::PubNubError, dx::subscribe::SubscribeCursor}; + use crate::{core::PubNubError, dx::subscribe::result::SubscribeResult}; + use futures::FutureExt; - #[test] - fn receive_messages() { - fn mock_receive_function( - channels: &Option>, - channel_groups: &Option>, - cursor: &SubscribeCursor, - attempt: u8, - reason: Option, - ) -> Result, PubNubError> { - assert_eq!(channels, &Some(vec!["ch1".to_string()])); - assert_eq!(channel_groups, &Some(vec!["cg1".to_string()])); - assert_eq!(attempt, 0); - assert_eq!(reason, None); - assert_eq!( - cursor, - &SubscribeCursor { - timetoken: 0, - region: 0 - } - ); + #[tokio::test] + async fn receive_messages() { + let mock_receive_function: Arc = Arc::new(move |params| { + assert_eq!(params.channels, &Some(vec!["ch1".to_string()])); + assert_eq!(params.channel_groups, &Some(vec!["cg1".to_string()])); + assert_eq!(params.attempt, 0); + assert_eq!(params.reason, None); + assert_eq!(params.cursor, Some(&Default::default())); + assert_eq!(params.effect_id, "id"); - Ok(vec![SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { - timetoken: 0, - region: 0, - }, - messages: vec![], - }]) - } + async move { + Ok(SubscribeResult { + cursor: Default::default(), + messages: vec![], + }) + } + .boxed() + }); let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), - &SubscribeCursor { - timetoken: 0, - region: 0, - }, - mock_receive_function, - ); + &Default::default(), + "id", + &mock_receive_function, + ) + .await; + assert!(!result.is_empty()); assert!(matches!( - result.unwrap().first().unwrap(), - &SubscribeEvent::ReceiveSuccess { .. } - )) + result.first().unwrap(), + SubscribeEvent::ReceiveSuccess { .. } + )); } - #[test] - fn return_handskahe_failure_event_on_err() { - fn mock_receive_function( - _channels: &Option>, - _channel_groups: &Option>, - _cursor: &SubscribeCursor, - _attempt: u8, - _reason: Option, - ) -> Result, PubNubError> { - Err(PubNubError::Transport { - details: "test".into(), - }) - } + #[tokio::test] + async fn return_receive_failure_event_on_err() { + let mock_receive_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: None, + }) + } + .boxed() + }); - let binding = execute( + let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), - &SubscribeCursor { - timetoken: 0, - region: 0, - }, - mock_receive_function, + &Default::default(), + "id", + &mock_receive_function, ) - .unwrap(); - let result = &binding[0]; + .await; - assert!(matches!(result, &SubscribeEvent::ReceiveFailure { .. })); + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + SubscribeEvent::ReceiveFailure { .. } + )); } } diff --git a/src/dx/subscribe/event_engine/effects/receive_reconnection.rs b/src/dx/subscribe/event_engine/effects/receive_reconnection.rs index 16ffb1ad..ad9bc343 100644 --- a/src/dx/subscribe/event_engine/effects/receive_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/receive_reconnection.rs @@ -1,119 +1,165 @@ -use crate::lib::alloc::{string::String, vec, vec::Vec}; use crate::{ - core::PubNubError, + core::{PubNubError, RequestRetryPolicy}, dx::subscribe::{ - event_engine::{ReceiveFunction, SubscribeEvent}, - SubscribeCursor, + event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, + SubscribeCursor, SubscriptionParams, }, + lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, }; +use futures::TryFutureExt; +use log::info; -pub(crate) fn execute( +#[allow(clippy::too_many_arguments)] +pub(crate) async fn execute( channels: &Option>, channel_groups: &Option>, cursor: &SubscribeCursor, attempt: u8, reason: PubNubError, - executor: ReceiveFunction, -) -> Option> { - Some( - executor(channels, channel_groups, cursor, attempt, Some(reason)) - .unwrap_or_else(|err| vec![SubscribeEvent::ReceiveReconnectFailure { reason: err }]), + effect_id: &str, + retry_policy: &RequestRetryPolicy, + executor: &Arc, +) -> Vec { + if !retry_policy.retriable(&attempt, Some(&reason)) { + return vec![SubscribeEvent::ReceiveReconnectGiveUp { reason }]; + } + + info!( + "Receive reconnection at {:?} for\nchannels: {:?}\nchannel groups: {:?}", + cursor.timetoken, + channels.as_ref().unwrap_or(&Vec::new()), + channel_groups.as_ref().unwrap_or(&Vec::new()), + ); + + executor(SubscriptionParams { + channels, + channel_groups, + cursor: Some(cursor), + attempt, + reason: Some(reason), + effect_id, + }) + .map_ok_or_else( + |error| { + log::debug!("Receive reconnection error: {:?}", error); + + (!matches!(error, PubNubError::EffectCanceled)) + .then(|| vec![SubscribeEvent::ReceiveReconnectFailure { reason: error }]) + .unwrap_or(vec![]) + }, + |subscribe_result| { + vec![SubscribeEvent::ReceiveReconnectSuccess { + cursor: subscribe_result.cursor, + messages: subscribe_result.messages, + }] + }, ) + .await } #[cfg(test)] mod should { use super::*; - use crate::{core::PubNubError, dx::subscribe::SubscribeCursor}; + use crate::{ + core::{PubNubError, TransportResponse}, + dx::subscribe::result::SubscribeResult, + lib::alloc::boxed::Box, + }; + use futures::FutureExt; - #[test] - fn receive_messages() { - fn mock_receive_function( - channels: &Option>, - channel_groups: &Option>, - cursor: &SubscribeCursor, - attempt: u8, - reason: Option, - ) -> Result, PubNubError> { - assert_eq!(channels, &Some(vec!["ch1".to_string()])); - assert_eq!(channel_groups, &Some(vec!["cg1".to_string()])); - assert_eq!(attempt, 10); + #[tokio::test] + async fn receive_reconnect() { + let mock_receive_function: Arc = Arc::new(move |params| { + assert_eq!(params.channels, &Some(vec!["ch1".to_string()])); + assert_eq!(params.channel_groups, &Some(vec!["cg1".to_string()])); + assert_eq!(params.attempt, 10); assert_eq!( - reason, + params.reason, Some(PubNubError::Transport { details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), }) ); - assert_eq!( - cursor, - &SubscribeCursor { - timetoken: 0, - region: 0 - } - ); + assert_eq!(params.cursor, Some(&Default::default())); + assert_eq!(params.effect_id, "id"); - Ok(vec![SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { - timetoken: 0, - region: 0, - }, - messages: vec![], - }]) - } + async move { + Ok(SubscribeResult { + cursor: Default::default(), + messages: vec![], + }) + } + .boxed() + }); let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), - &SubscribeCursor { - timetoken: 0, - region: 0, - }, + &Default::default(), 10, PubNubError::Transport { details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), }, - mock_receive_function, - ); + "id", + &RequestRetryPolicy::None, + &mock_receive_function, + ) + .await; + assert!(!result.is_empty()); assert!(matches!( - result.unwrap().first().unwrap(), - &SubscribeEvent::ReceiveSuccess { .. } - )) + result.first().unwrap(), + SubscribeEvent::ReceiveReconnectSuccess { .. } + )); } - #[test] - fn return_handskahe_failure_event_on_err() { - fn mock_receive_function( - _channels: &Option>, - _channel_groups: &Option>, - _cursor: &SubscribeCursor, - _attempt: u8, - _reason: Option, - ) -> Result, PubNubError> { - Err(PubNubError::Transport { - details: "test".into(), - }) - } + #[tokio::test] + async fn return_receive_reconnect_failure_event_on_err() { + 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 binding = execute( + let result = execute( &Some(vec!["ch1".to_string()]), &Some(vec!["cg1".to_string()]), - &SubscribeCursor { - timetoken: 0, - region: 0, - }, + &Default::default(), 10, PubNubError::Transport { details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }, + "id", + &RequestRetryPolicy::Linear { + delay: 0, + max_retry: 1, }, - mock_receive_function, + &mock_receive_function, ) - .unwrap(); - let result = &binding[0]; + .await; + assert!(!result.is_empty()); assert!(matches!( - result, - &SubscribeEvent::ReceiveReconnectFailure { .. } + result.first().unwrap(), + SubscribeEvent::ReceiveReconnectGiveUp { .. } )); } } diff --git a/src/dx/subscribe/event_engine/event.rs b/src/dx/subscribe/event_engine/event.rs index 46bd3cf1..3f54e505 100644 --- a/src/dx/subscribe/event_engine/event.rs +++ b/src/dx/subscribe/event_engine/event.rs @@ -1,3 +1,4 @@ +use crate::dx::subscribe::result::Update; use crate::{ core::{event_engine::Event, PubNubError}, dx::subscribe::SubscribeCursor, @@ -9,6 +10,7 @@ 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. /// @@ -77,7 +79,7 @@ pub(crate) enum SubscribeEvent { /// [`PubNub`]: https://www.pubnub.com/ ReceiveSuccess { cursor: SubscribeCursor, - messages: Vec, + messages: Vec, }, /// Receive updates completed with error. @@ -97,7 +99,7 @@ pub(crate) enum SubscribeEvent { /// [`PubNub`]: https://www.pubnub.com/ ReceiveReconnectSuccess { cursor: SubscribeCursor, - messages: Vec, + messages: Vec, }, /// Receive updates reconnect completed with error. @@ -129,6 +131,12 @@ pub(crate) enum SubscribeEvent { /// /// [`PubNub`]: https://www.pubnub.com/ Reconnect, + + /// Unsubscribe from all channels and groups. + /// + /// Emitted when explicitly requested by user to leave all channels and + /// groups. + UnsubscribeAll, } impl Event for SubscribeEvent { @@ -148,6 +156,7 @@ impl Event for SubscribeEvent { SubscribeEvent::ReceiveReconnectGiveUp { .. } => "RECEIVE_RECONNECT_GIVEUP", SubscribeEvent::Disconnect => "DISCONNECT", SubscribeEvent::Reconnect => "RECONNECT", + SubscribeEvent::UnsubscribeAll => "UNSUBSCRIBE_ALL", } } } diff --git a/src/dx/subscribe/event_engine/invocation.rs b/src/dx/subscribe/event_engine/invocation.rs index 09ebfec9..17bfd437 100644 --- a/src/dx/subscribe/event_engine/invocation.rs +++ b/src/dx/subscribe/event_engine/invocation.rs @@ -1,10 +1,13 @@ -use crate::dx::subscribe::event_engine::SubscribeEvent; use crate::{ core::{event_engine::EffectInvocation, PubNubError}, - dx::subscribe::{event_engine::SubscribeEffect, SubscribeCursor, SubscribeStatus}, + dx::subscribe::{ + event_engine::{SubscribeEffect, SubscribeEvent}, + result::Update, + SubscribeCursor, SubscribeStatus, + }, lib::{ alloc::{string::String, vec::Vec}, - core::fmt::{Formatter, Result}, + core::fmt::{Display, Formatter, Result}, }, }; @@ -118,7 +121,7 @@ pub(crate) enum SubscribeEffectInvocation { EmitStatus(SubscribeStatus), /// Received updates notification effect invocation. - EmitMessages(Vec), + EmitMessages(Vec), } impl EffectInvocation for SubscribeEffectInvocation { @@ -127,16 +130,16 @@ impl EffectInvocation for SubscribeEffectInvocation { fn id(&self) -> &str { match self { - Self::Handshake { .. } => "Handshake", - Self::CancelHandshake => "CancelHandshake", - Self::HandshakeReconnect { .. } => "HandshakeReconnect", - Self::CancelHandshakeReconnect => "CancelHandshakeReconnect", - Self::Receive { .. } => "Receive", - Self::CancelReceive { .. } => "CancelReceive", - Self::ReceiveReconnect { .. } => "ReceiveReconnect", - Self::CancelReceiveReconnect { .. } => "CancelReceiveReconnect", - Self::EmitStatus(_status) => "EmitStatus", - Self::EmitMessages(_messages) => "EmitMessages", + Self::Handshake { .. } => "HANDSHAKE", + Self::CancelHandshake => "CANCEL_HANDSHAKE", + Self::HandshakeReconnect { .. } => "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", } } @@ -172,19 +175,19 @@ impl EffectInvocation for SubscribeEffectInvocation { } } -impl core::fmt::Display for SubscribeEffectInvocation { +impl Display for SubscribeEffectInvocation { fn fmt(&self, f: &mut Formatter<'_>) -> Result { match self { - Self::Handshake { .. } => write!(f, "Handshake"), - Self::CancelHandshake => write!(f, "CancelHandshake"), - Self::HandshakeReconnect { .. } => write!(f, "HandshakeReconnect"), - Self::CancelHandshakeReconnect => write!(f, "CancelHandshakeReconnect"), - Self::Receive { .. } => write!(f, "Receive"), - Self::CancelReceive { .. } => write!(f, "CancelReceive"), - Self::ReceiveReconnect { .. } => write!(f, "ReceiveReconnect"), - Self::CancelReceiveReconnect { .. } => write!(f, "CancelReceiveReconnect"), - Self::EmitStatus(status) => write!(f, "EmitStatus({})", status), - Self::EmitMessages(messages) => write!(f, "EmitMessages({:?})", messages), + Self::Handshake { .. } => write!(f, "HANDSHAKE"), + Self::CancelHandshake => write!(f, "CANCEL_HANDSHAKE"), + Self::HandshakeReconnect { .. } => write!(f, "HANDSHAKE_RECONNECT"), + Self::CancelHandshakeReconnect => write!(f, "CANCEL_HANDSHAKE_RECONNECT"), + Self::Receive { .. } => write!(f, "RECEIVE_MESSAGES"), + 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), } } } diff --git a/src/dx/subscribe/event_engine/mod.rs b/src/dx/subscribe/event_engine/mod.rs index 87c70d6a..a0146351 100644 --- a/src/dx/subscribe/event_engine/mod.rs +++ b/src/dx/subscribe/event_engine/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod effects; #[doc(inline)] #[allow(unused_imports)] -pub(crate) use effect_handler::{HandshakeFunction, ReceiveFunction, SubscribeEffectHandler}; +pub(crate) use effect_handler::SubscribeEffectHandler; pub(crate) mod effect_handler; #[doc(inline)] @@ -21,3 +21,64 @@ pub(crate) mod event; #[allow(unused_imports)] pub(crate) use state::SubscribeState; pub(crate) mod state; + +use crate::{ + core::event_engine::EventEngine, + lib::alloc::{string::String, vec::Vec}, +}; + +pub(crate) type SubscribeEventEngine = + EventEngine; + +impl + EventEngine +{ + #[allow(dead_code)] + pub(in crate::dx::subscribe) fn current_subscription( + &self, + ) -> (Option>, Option>) { + match self.current_state() { + SubscribeState::Handshaking { + channels, + channel_groups, + .. + } + | SubscribeState::HandshakeReconnecting { + channels, + channel_groups, + .. + } + | SubscribeState::HandshakeStopped { + channels, + channel_groups, + .. + } + | SubscribeState::HandshakeFailed { + channels, + channel_groups, + .. + } + | SubscribeState::Receiving { + channels, + channel_groups, + .. + } + | SubscribeState::ReceiveReconnecting { + channels, + channel_groups, + .. + } + | SubscribeState::ReceiveStopped { + channels, + channel_groups, + .. + } + | SubscribeState::ReceiveFailed { + channels, + channel_groups, + .. + } => (channels, channel_groups), + _ => (None, None), + } + } +} diff --git a/src/dx/subscribe/event_engine/state.rs b/src/dx/subscribe/event_engine/state.rs index 910e1f04..acbe78ab 100644 --- a/src/dx/subscribe/event_engine/state.rs +++ b/src/dx/subscribe/event_engine/state.rs @@ -12,6 +12,7 @@ use crate::{ }, SubscribeEvent, }, + result::Update, SubscribeCursor, SubscribeStatus, }, lib::alloc::{string::String, vec, vec::Vec}, @@ -234,17 +235,31 @@ impl SubscribeState { }, None, )), + Self::HandshakeStopped { .. } => Some(self.transition_to( + Self::HandshakeStopped { + channels: channels.clone(), + channel_groups: channel_groups.clone(), + }, + None, + )), Self::Receiving { cursor, .. } | Self::ReceiveReconnecting { cursor, .. } | Self::ReceiveFailed { cursor, .. } => Some(self.transition_to( Self::Receiving { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), + }, + None, + )), + Self::ReceiveStopped { cursor, .. } => Some(self.transition_to( + Self::ReceiveStopped { + channels: channels.clone(), + channel_groups: channel_groups.clone(), + cursor: cursor.clone(), }, None, )), - _ => None, } } @@ -269,11 +284,20 @@ impl SubscribeState { Self::Receiving { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), }, None, )), - _ => None, + Self::HandshakeStopped { .. } | Self::ReceiveStopped { .. } => { + Some(self.transition_to( + Self::ReceiveStopped { + channels: channels.clone(), + channel_groups: channel_groups.clone(), + cursor: cursor.clone(), + }, + None, + )) + } } } @@ -298,7 +322,7 @@ impl SubscribeState { Self::Receiving { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), }, Some(vec![EmitStatus(SubscribeStatus::Connected)]), )), @@ -374,7 +398,9 @@ impl SubscribeState { channel_groups: channel_groups.clone(), reason: reason.clone(), }, - None, + Some(vec![EmitStatus(SubscribeStatus::ConnectedError( + reason.clone(), + ))]), )), _ => None, } @@ -387,7 +413,7 @@ impl SubscribeState { fn receive_success_transition( &self, cursor: &SubscribeCursor, - messages: &[String], + messages: &[Update], ) -> Option> { match self { Self::Receiving { @@ -403,7 +429,7 @@ impl SubscribeState { Self::Receiving { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), }, Some(vec![ EmitMessages(messages.to_vec()), @@ -429,8 +455,8 @@ impl SubscribeState { Self::ReceiveReconnecting { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, - attempts: 0, + cursor: cursor.clone(), + attempts: 1, reason: reason.clone(), }, None, @@ -458,7 +484,7 @@ impl SubscribeState { Self::ReceiveReconnecting { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), attempts: attempts + 1, reason: reason.clone(), }, @@ -486,7 +512,7 @@ impl SubscribeState { Self::ReceiveFailed { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), reason: reason.clone(), }, Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), @@ -530,7 +556,7 @@ impl SubscribeState { Self::ReceiveStopped { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), }, Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), )), @@ -575,13 +601,21 @@ impl SubscribeState { Self::Receiving { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), }, None, )), _ => None, } } + + /// Handle unsubscribe all event. + fn unsubscribe_all_transition(&self) -> Option> { + Some(self.transition_to( + Self::Unsubscribed, + Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), + )) + } } impl State for SubscribeState { @@ -616,7 +650,7 @@ impl State for SubscribeState { } => Some(vec![Receive { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), }]), Self::ReceiveReconnecting { channels, @@ -627,7 +661,7 @@ impl State for SubscribeState { } => Some(vec![ReceiveReconnect { channels: channels.clone(), channel_groups: channel_groups.clone(), - cursor: *cursor, + cursor: cursor.clone(), attempts: *attempts, reason: reason.clone(), }]), @@ -682,6 +716,7 @@ impl State for SubscribeState { } SubscribeEvent::Disconnect => self.disconnect_transition(), SubscribeEvent::Reconnect => self.reconnect_transition(), + SubscribeEvent::UnsubscribeAll => self.unsubscribe_all_transition(), } } @@ -705,49 +740,62 @@ impl State for SubscribeState { #[cfg(test)] mod should { + // TODO: EE process tests should be async! + use super::*; - use crate::core::event_engine::EventEngine; - use crate::dx::subscribe::event_engine::effect_handler::EmitData; - use crate::dx::subscribe::event_engine::{SubscribeEffect, SubscribeEffectHandler}; + use crate::core::RequestRetryPolicy; + use crate::providers::futures_tokio::TokioRuntime; + use crate::{ + core::event_engine::EventEngine, + dx::subscribe::{ + event_engine::{ + effects::{ + EmitMessagesEffectExecutor, EmitStatusEffectExecutor, SubscribeEffectExecutor, + }, + SubscribeEffect, SubscribeEffectHandler, + }, + result::SubscribeResult, + }, + lib::alloc::sync::Arc, + }; + use futures::FutureExt; use test_case::test_case; - fn handshake_function( - _channels: &Option>, - _channel_groups: &Option>, - _attempt: u8, - _reason: Option, - ) -> Result, PubNubError> { - // Do nothing. - Ok(vec![]) - } - - fn receive_function( - _channels: &Option>, - _channel_groups: &Option>, - _cursor: &SubscribeCursor, - _attempt: u8, - _reason: Option, - ) -> Result, PubNubError> { - // Do nothing. - Ok(vec![]) - } - - fn emit_function(_data: EmitData) -> Result<(), PubNubError> { - // Do nothing. - Ok(()) - } - fn event_engine( start_state: SubscribeState, - ) -> EventEngine< - SubscribeState, - SubscribeEffectHandler, - SubscribeEffect, - SubscribeEffectInvocation, + ) -> Arc< + EventEngine< + SubscribeState, + SubscribeEffectHandler, + SubscribeEffect, + SubscribeEffectInvocation, + >, > { + let call: Arc = Arc::new(|_| { + async move { + Ok(SubscribeResult { + cursor: Default::default(), + messages: vec![], + }) + } + .boxed() + }); + + let emit_status: Arc = Arc::new(|_| {}); + let emit_message: Arc = Arc::new(|_| {}); + + let (tx, _) = async_channel::bounded(1); + EventEngine::new( - SubscribeEffectHandler::new(handshake_function, receive_function, emit_function), + SubscribeEffectHandler::new( + call, + emit_status, + emit_message, + RequestRetryPolicy::None, + tx, + ), start_state, + TokioRuntime, ) } @@ -768,24 +816,25 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::Unsubscribed, SubscribeEvent::ReceiveFailure { - reason: PubNubError::Transport { details: "Test".to_string(), } + reason: PubNubError::Transport { details: "Test".to_string(), response: None } }, SubscribeState::Unsubscribed; "to not change on unexpected event" )] - fn transition_for_unsubscribed_state( + #[tokio::test] + async fn transition_for_unsubscribed_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -820,13 +869,13 @@ mod should { channel_groups: Some(vec!["gr1".to_string()]) }, SubscribeEvent::HandshakeFailure { - reason: PubNubError::Transport { details: "Test reason".to_string() }, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), attempts: 1, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }; "to handshake reconnect on handshake failure" )] @@ -847,11 +896,13 @@ mod should { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]) }, - SubscribeEvent::HandshakeSuccess { cursor: SubscribeCursor { timetoken: 10, region: 1 } }, + SubscribeEvent::HandshakeSuccess { + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } + }, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on handshake success" )] @@ -863,12 +914,12 @@ mod should { SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on subscription restored" )] @@ -878,7 +929,7 @@ mod should { channel_groups: Some(vec!["gr1".to_string()]) }, SubscribeEvent::HandshakeReconnectGiveUp { - reason: PubNubError::Transport { details: "Test reason".to_string(), } + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }, SubscribeState::Handshaking { channels: Some(vec!["ch1".to_string()]), @@ -886,7 +937,8 @@ mod should { }; "to not change on unexpected event" )] - fn transition_handshaking_state( + #[tokio::test] + async fn transition_handshaking_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -903,17 +955,17 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::HandshakeReconnectFailure { - reason: PubNubError::Transport { details: "Test reason on error".to_string() }, + reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 1, - reason: PubNubError::Transport { details: "Test reason on error".to_string() }, + attempts: 2, + reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }; "to handshake reconnecting on reconnect failure" )] @@ -921,8 +973,8 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), @@ -938,8 +990,8 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { @@ -952,16 +1004,16 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::HandshakeReconnectGiveUp { - reason: PubNubError::Transport { details: "Test give up reason".to_string() } + reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }, SubscribeState::HandshakeFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - reason: PubNubError::Transport { details: "Test give up reason".to_string() } + reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }; "to handshake failed on give up" )] @@ -969,16 +1021,16 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::HandshakeReconnectSuccess { - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on reconnect success" )] @@ -986,18 +1038,18 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + 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, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on subscription restored" )] @@ -1005,22 +1057,23 @@ mod should { SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, messages: vec![] }, SubscribeState::HandshakeReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }; "to not change on unexpected event" )] - fn transition_handshake_reconnecting_state( + #[tokio::test] + async fn transition_handshake_reconnecting_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -1037,7 +1090,7 @@ mod should { SubscribeState::HandshakeFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - reason: PubNubError::Transport { details: "Test reason".to_string() }, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), @@ -1053,7 +1106,7 @@ mod should { SubscribeState::HandshakeFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - reason: PubNubError::Transport { details: "Test reason".to_string() }, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { @@ -1066,17 +1119,17 @@ mod should { SubscribeState::HandshakeFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - reason: PubNubError::Transport { details: "Test reason".to_string() }, + 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, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on subscription restored" )] @@ -1084,20 +1137,21 @@ mod should { SubscribeState::HandshakeFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - reason: PubNubError::Transport { details: "Test reason".to_string() }, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, messages: vec![] }, SubscribeState::HandshakeFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - reason: PubNubError::Transport { details: "Test reason".to_string() }, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }; "to not change on unexpected event" )] - fn transition_handshake_failed_state( + #[tokio::test] + async fn transition_handshake_failed_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -1128,7 +1182,7 @@ mod should { channel_groups: Some(vec!["gr1".to_string()]), }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, messages: vec![] }, SubscribeState::HandshakeStopped { @@ -1137,7 +1191,8 @@ mod should { }; "to not change on unexpected event" )] - fn transition_handshake_stopped_state( + #[tokio::test] + async fn transition_handshake_stopped_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -1154,7 +1209,7 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), @@ -1163,7 +1218,7 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] @@ -1171,17 +1226,17 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionRestored { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on subscription restored" )] @@ -1189,16 +1244,16 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::ReceiveSuccess { - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, messages: vec![] }, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on receive success" )] @@ -1206,17 +1261,17 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::ReceiveFailure { - reason: PubNubError::Transport { details: "Test reason".to_string() } + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }, SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test reason".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }; "to receive reconnecting on receive failure" )] @@ -1224,13 +1279,13 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::Disconnect, SubscribeState::ReceiveStopped { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on disconnect" )] @@ -1238,19 +1293,20 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to not change on unexpected event" )] - fn transition_receiving_state( + #[tokio::test] + async fn transition_receiving_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -1267,19 +1323,19 @@ mod should { SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::ReceiveReconnectFailure { - reason: PubNubError::Transport { details: "Test reconnect error".to_string() } + reason: PubNubError::Transport { details: "Test reconnect error".to_string(), response: None, } }, SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 1, - reason: PubNubError::Transport { details: "Test reconnect error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 2, + reason: PubNubError::Transport { details: "Test reconnect error".to_string(), response: None, } }; "to receive reconnecting on reconnect failure" )] @@ -1287,9 +1343,9 @@ mod should { SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), @@ -1298,7 +1354,7 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] @@ -1306,19 +1362,19 @@ mod should { SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { 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, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on subscription restored" )] @@ -1326,15 +1382,15 @@ mod should { SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::Disconnect, SubscribeState::ReceiveStopped { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on disconnect" )] @@ -1342,18 +1398,18 @@ mod should { SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::ReceiveReconnectGiveUp { - reason: PubNubError::Transport { details: "Test give up error".to_string() } + reason: PubNubError::Transport { details: "Test give up error".to_string(), response: None, } }, SubscribeState::ReceiveFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - reason: PubNubError::Transport { details: "Test give up error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test give up error".to_string(), response: None, } }; "to receive failed on give up" )] @@ -1361,23 +1417,24 @@ mod should { SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::ReceiveReconnecting { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - attempts: 0, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + attempts: 1, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }; "to not change on unexpected event" )] - fn transition_receiving_reconnecting_state( + #[tokio::test] + async fn transition_receiving_reconnecting_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -1394,8 +1451,8 @@ mod should { SubscribeState::ReceiveFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::SubscriptionChanged { channels: Some(vec!["ch2".to_string()]), @@ -1404,7 +1461,7 @@ mod should { SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] @@ -1412,18 +1469,18 @@ mod should { SubscribeState::ReceiveFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { 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, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { channels: Some(vec!["ch2".to_string()]), channel_groups: Some(vec!["gr2".to_string()]), - cursor: SubscribeCursor { timetoken: 100, region: 1 }, + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on subscription restored" )] @@ -1431,14 +1488,14 @@ mod should { SubscribeState::ReceiveFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::Reconnect, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on reconnect" )] @@ -1446,21 +1503,22 @@ mod should { SubscribeState::ReceiveFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: 100, region: 1 } + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 } }, SubscribeState::ReceiveFailed { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, - reason: PubNubError::Transport { details: "Test error".to_string() } + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, + reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }; "to not change on unexpected event" )] - fn transition_receive_failed_state( + #[tokio::test] + async fn transition_receive_failed_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, @@ -1476,13 +1534,13 @@ mod should { SubscribeState::ReceiveStopped { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::Reconnect, SubscribeState::Receiving { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on reconnect" )] @@ -1490,19 +1548,20 @@ mod should { SubscribeState::ReceiveStopped { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::HandshakeSuccess { - cursor: SubscribeCursor { timetoken: 100, region: 1 } + cursor: SubscribeCursor { timetoken: "100".into(), region: 1 } }, SubscribeState::ReceiveStopped { channels: Some(vec!["ch1".to_string()]), channel_groups: Some(vec!["gr1".to_string()]), - cursor: SubscribeCursor { timetoken: 10, region: 1 }, + cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to not change on unexpected event" )] - fn transition_receive_stopped_state( + #[tokio::test] + async fn transition_receive_stopped_state( init_state: SubscribeState, event: SubscribeEvent, target_state: SubscribeState, diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index 4f4fa4e0..a0e9d149 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -4,10 +4,519 @@ pub(crate) mod event_engine; -#[doc(inline)] -pub use types::{SubscribeCursor, SubscribeStatus}; +use event_engine::{SubscribeEffectHandler, SubscribeState}; + +use futures::{ + future::{ready, BoxFuture}, + FutureExt, +}; + +#[cfg(feature = "serde")] +use crate::providers::deserialization_serde::SerdeDeserializer; +pub use result::{SubscribeResponseBody, Update}; +pub mod result; + +#[doc(inline)] +pub use types::{ + File, MessageAction, Object, Presence, SubscribeCursor, SubscribeMessageType, SubscribeStatus, + SubscribeStreamEvent, +}; pub mod types; -#[allow(dead_code)] -pub(crate) mod subscription; +use crate::{ + core::{event_engine::EventEngine, runtime::Runtime, PubNubError, Transport}, + dx::{pubnub_client::PubNubClientInstance, subscribe::result::SubscribeResult}, + lib::alloc::{borrow::ToOwned, boxed::Box, string::String, sync::Arc, vec::Vec}, +}; + +pub(crate) use subscription_manager::SubscriptionManager; +pub(crate) mod subscription_manager; + +pub(crate) use subscription_configuration::{ + SubscriptionConfiguration, SubscriptionConfigurationRef, +}; +pub(crate) mod subscription_configuration; + +#[doc(inline)] +pub use builders::*; +pub mod builders; + +#[doc(inline)] +use cancel::CancellationTask; +mod cancel; + +#[derive(Clone)] +pub(crate) struct SubscriptionParams<'execution> { + channels: &'execution Option>, + channel_groups: &'execution Option>, + cursor: Option<&'execution SubscribeCursor>, + attempt: u8, + reason: Option, + effect_id: &'execution str, +} + +impl PubNubClientInstance +where + T: Transport + Send + 'static, +{ + /// Create subscription listener. + /// + /// Listeners configure [`PubNubClient`] to receive real-time updates for + /// specified list of channels and groups. + /// + /// ```no_run // Starts listening for real-time updates + /// use futures::StreamExt; + /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// + /// # #[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(()) + /// # } + /// + /// ``` + /// + /// For more examples see our [examples directory](https://github.com/pubnub/rust/tree/master/examples). + /// + /// Instance of [`SubscriptionBuilder`] returned. + /// [`PubNubClient`]: crate::PubNubClient + #[cfg(all(feature = "tokio", feature = "serde"))] + pub fn subscribe(&self) -> SubscriptionBuilder { + use crate::providers::futures_tokio::TokioRuntime; + + self.subscribe_with_runtime(TokioRuntime) + } + + /// Create subscription listener. + /// + /// Listeners configure [`PubNubClient`] to receive real-time updates for + /// specified list of channels and groups. + /// + /// ```no_run // Starts listening for real-time updates + /// use futures::StreamExt; + /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// + /// # #[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(()) + /// # } + /// + /// ``` + /// + /// Instance of [`SubscriptionWithDeserializerBuilder`] returned. + /// [`PubNubClient`]: crate::PubNubClient + #[cfg(all(feature = "tokio", not(feature = "serde")))] + pub fn subscribe(&self) -> SubscriptionWithDeserializerBuilder { + use crate::providers::futures_tokio::TokioRuntime; + + self.subscribe_with_runtime(TokioRuntime) + } + + /// Create subscription listener. + /// + /// Listeners configure [`PubNubClient`] to receive real-time updates for + /// specified list of channels and groups. + /// + /// It takes custom runtime which will be used for detached tasks spawning + /// and delayed task execution. + /// + /// ```no_run // Starts listening for real-time updates + /// use futures::StreamExt; + /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// use pubnub::core::runtime::Runtime; + /// use std::future::Future; + /// + /// #[derive(Clone)] + /// struct MyRuntime; + /// + /// #[async_trait::async_trait] + /// impl Runtime for MyRuntime { + /// fn spawn(&self, future: impl Future + Send + 'static) { + /// // spawn the Future + /// // e.g. tokio::spawn(future); + /// } + /// + /// async fn sleep(self, _delay: u64) { + /// // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await + /// } + /// } + /// # #[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_with_runtime(MyRuntime) + /// .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(()) + /// # } + /// + /// ``` + /// + /// Instance of [`SubscriptionBuilder`] returned. + /// [`PubNubClient`]: crate::PubNubClient + #[cfg(feature = "serde")] + pub fn subscribe_with_runtime(&self, runtime: R) -> SubscriptionBuilder + where + R: Runtime + Send + Sync + 'static, + { + { + // Initialize subscription module when it will be first required. + let mut subscription_slot = self.subscription.write(); + if subscription_slot.is_none() { + *subscription_slot = Some(SubscriptionConfiguration { + inner: Arc::new(SubscriptionConfigurationRef { + subscription_manager: self.clone().subscription_manager(runtime), + deserializer: Some(Arc::new(SerdeDeserializer)), + }), + }); + } + } + + SubscriptionBuilder { + subscription: Some(self.subscription.clone()), + ..Default::default() + } + } + + /// Create subscription listener. + /// + /// Listeners configure [`PubNubClient`] to receive real-time updates for + /// specified list of channels and groups. + /// + /// It takes custom runtime which will be used for detached tasks spawning + /// and delayed task execution. + /// ```no_run // Starts listening for real-time updates + /// use futures::StreamExt; + /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; + /// use pubnub::core::runtime::Runtime; + /// use std::future::Future; + /// + /// #[derive(Clone)] + /// struct MyRuntime; + /// + /// impl Runtime for MyRuntime { + /// fn spawn(&self, future: impl Future + Send + 'static) { + /// // spawn the Future + /// // e.g. tokio::spawn(future); + /// } + /// } + /// # #[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_with_runtime(MyRuntime) + /// .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(()) + /// # } + /// + /// ``` + /// + /// Instance of [`SubscriptionWithDeserializerBuilder`] returned. + /// [`PubNubClient`]: crate::PubNubClient + #[cfg(not(feature = "serde"))] + pub fn subscribe_with_runtime(&self, runtime: R) -> SubscriptionWithDeserializerBuilder + where + R: Runtime + Send + Sync + 'static, + { + { + // Initialize subscription module when it will be first required. + let mut subscription_slot = self.subscription.write(); + if subscription_slot.is_none() { + *subscription_slot = Some(SubscriptionConfiguration { + inner: Arc::new(SubscriptionConfigurationRef { + subscription_manager: self.clone().subscription_manager(runtime), + deserializer: None, + }), + }); + } + } + + SubscriptionWithDeserializerBuilder { + subscription: self.subscription.clone(), + } + } + + /// Create subscribe request builder. + /// This method is used to create events stream for real-time updates on + /// passed list of channels and groups. + /// + /// Instance of [`SubscribeRequestBuilder`] returned. + pub(crate) fn subscribe_request(&self) -> SubscribeRequestBuilder { + SubscribeRequestBuilder { + pubnub_client: Some(self.clone()), + ..Default::default() + } + } + + pub(crate) fn subscription_manager(&mut self, runtime: R) -> SubscriptionManager + where + R: Runtime + Send + Sync + 'static, + { + let channel_bound = 10; // TODO: Think about this value + 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 runtime_sleep = runtime.clone(); + + let (cancel_tx, cancel_rx) = async_channel::bounded::(channel_bound); + + let engine = 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 inner_runtime_sleep = runtime_sleep.clone(); + + Self::subscribe_call( + subscribe_client.clone(), + params.clone(), + Arc::new(move || { + if let Some(de) = delay_in_secs { + // let rt = inner_runtime_sleep.clone(); + inner_runtime_sleep.clone().sleep(de).boxed() + } else { + ready(()).boxed() + } + }), + cancel_rx.clone(), + ) + }), + 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) + })), + request_retry_policy, + cancel_tx, + ), + SubscribeState::Unsubscribed, + runtime, + ); + + SubscriptionManager::new(engine) + } + + pub(crate) fn subscribe_call( + client: Self, + params: SubscriptionParams, + delay: Arc, + cancel_rx: async_channel::Receiver, + ) -> BoxFuture<'static, Result> + where + F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, + { + let mut request = client + .subscribe_request() + .cursor(params.cursor.cloned().unwrap_or_default()); // TODO: is this clone required? + + if let Some(channels) = params.channels.clone() { + request = request.channels(channels); + } + + if let Some(channel_groups) = params.channel_groups.clone() { + request = request.channel_groups(channel_groups); + } + + let deserializer = { + let subscription = client + .subscription + .read() + .clone() + .expect("Subscription configuration is missing"); + subscription + .deserializer + .clone() + .expect("Deserializer is missing") + }; + + let cancel_task = CancellationTask::new(cancel_rx, params.effect_id.to_owned()); // TODO: needs to be owned? + + request.execute(deserializer, delay, cancel_task).boxed() + } + + fn emit_status(client: Self, status: &SubscribeStatus) { + if let Some(manager) = client.subscription.read().as_ref() { + manager.subscription_manager.notify_new_status(status) + } + } + + fn emit_messages(client: Self, messages: Vec) { + let messages = if let Some(cryptor) = &client.cryptor { + messages + .into_iter() + .map(|update| update.decrypt(cryptor)) + .collect() + } else { + messages + }; + + if let Some(manager) = client.subscription.read().as_ref() { + manager.subscription_manager.notify_new_messages(messages) + } + } +} + +#[cfg(feature = "blocking")] +impl PubNubClientInstance where T: crate::core::blocking::Transport {} + +#[cfg(test)] +mod should { + use super::*; + use crate::{ + core::{TransportRequest, TransportResponse}, + Keyset, PubNubClientBuilder, PubNubGenericClient, + }; + + struct MockTransport; + + #[async_trait::async_trait] + impl crate::core::Transport for MockTransport { + async fn send(&self, _request: TransportRequest) -> Result { + Ok(TransportResponse { + status: 200, + headers: [].into(), + body: Some( + r#"{ + "t": { + "t": "15628652479932717", + "r": 4 + }, + "m": [ + { "a": "1", + "f": 514, + "i": "pn-0ca50551-4bc8-446e-8829-c70b704545fd", + "s": 1, + "p": { + "t": "15628652479933927", + "r": 4 + }, + "k": "demo", + "c": "my-channel", + "d": "my message", + "b": "my-channel" + } + ] + }"# + .into(), + ), + }) + } + } + + fn client() -> PubNubGenericClient { + PubNubClientBuilder::with_transport(MockTransport) + .with_keyset(Keyset { + subscribe_key: "demo", + publish_key: Some("demo"), + secret_key: None, + }) + .with_user_id("user") + .build() + .unwrap() + } + + #[tokio::test] + async fn create_builder() { + let _ = client().subscribe(); + } + + #[tokio::test] + async fn subscribe() { + env_logger::init(); + let subscription = client() + .subscribe() + .channels(["world".into()].to_vec()) + .execute() + .unwrap(); + + use futures::StreamExt; + let status = subscription.stream().next().await.unwrap(); + + assert!(matches!( + status, + SubscribeStreamEvent::Status(SubscribeStatus::Connected) + )); + } +} diff --git a/src/dx/subscribe/result.rs b/src/dx/subscribe/result.rs new file mode 100644 index 00000000..2171be4a --- /dev/null +++ b/src/dx/subscribe/result.rs @@ -0,0 +1,607 @@ +//! Subscribe result module. +//! +//! This module contains [`SubscribeResult`] type. +//! The [`SubscribeResult`] type is used to represent results of subscribe +//! operation. + +use crate::{ + core::{APIErrorBody, PubNubError, ScalarValue}, + dx::subscribe::{ + types::Message, + File, MessageAction, Object, Presence, {SubscribeCursor, SubscribeMessageType}, + }, + lib::{ + alloc::{ + boxed::Box, + string::{String, ToString}, + vec, + vec::Vec, + }, + collections::HashMap, + core::fmt::Debug, + }, +}; + +/// The result of a subscribe operation. +/// It contains next subscription cursor and list of real-time updates. +#[derive(Debug)] +pub struct SubscribeResult { + /// Time cursor for subscription loop. + /// + /// Next time cursor which can be used to fetch newer updates or + /// catchup / restore subscription from specific point in time. + pub cursor: SubscribeCursor, + + /// Received real-time updates. + /// + /// A few more real-time updates are generated by the [`PubNub`] network, in + /// addition to published messages and signals: + /// * `presence` – changes in channel's presence or associated user state + /// * `message actions` – change of action associated with specific message + /// * `objects` – changes in one of objects or their relationship: `uuid`, + /// `channel` or `membership` + /// * `file` – file sharing updates + /// + /// [`PubNub`]:https://www.pubnub.com/ + pub messages: Vec, +} + +/// Real-time update object. +/// +/// Each object represent specific real-time event and provide sufficient +/// information about it. +#[derive(Debug, Clone)] +pub enum Update { + /// Presence change real-time update. + /// + /// Payload represents one of the presence types: + /// * `join` – new user joined the channel + /// * `leave` – some user left channel + /// * `timeout` – service didn't notice user for a while + /// * `interval` – bulk update on `joined`, `left` and `timeout` users. + /// * `state-change` - some user changed state associated with him on channel. + Presence(Presence), + + /// Object real-time update. + Object(Object), + + /// Message's actions real-time update. + MessageAction(MessageAction), + + /// File sharing real-time update. + File(File), + + /// Real-time message update. + Message(Message), + + /// Real-time signal update. + Signal(Message), +} + +/// [`PubNub API`] raw response for subscribe request. +/// +/// +/// [`PubNub API`]: https://www.pubnub.com/docs +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +pub enum SubscribeResponseBody { + /// This is success response body for subscribe operation in the Subscriber + /// service. + /// It contains information about next time cursor and list of updates which + /// happened since previous time cursor. + /// + /// # Example + /// ```json + /// { + /// "t": { + /// "t": "16866076578137008", + /// "r": 43 + /// }, + /// "m": [ + /// { + /// "a": "1", + /// "f": 0, + /// "i": "moon", + /// "p": { + /// "t": "16866076578137008", + /// "r": 40 + /// }, + /// "c": "test_channel", + /// "d": "this can be base64 of encrypted message", + /// "b": "test_channel" + /// }, + /// { + /// "a": "1", + /// "f": 0, + /// "i": "moon", + /// "p": { + /// "t": "16866076578137108", + /// "r": 40 + /// }, + /// "c": "test_channel", + /// "d": { + /// "sender": "me", + /// "data": { + /// "secret": "here" + /// } + /// }, + /// "b": "test_channel" + /// }, + /// { + /// "a": "1", + /// "f": 0, + /// "i": "moon", + /// "p": { + /// "t": "16866076578137208", + /// "r": 40 + /// }, + /// "c": "test_channel", + /// "d": { + /// "ping_type": "echo", + /// "value": 16 + /// }, + /// "b": "test_channel" + /// } + /// ] + /// } + /// ``` + SuccessResponse(APISuccessBody), + + /// This is an error response body for a subscribe operation in the + /// Subscribe service. + /// It contains information about the service that provided the response and + /// details of what exactly was wrong. + /// + /// # Example + /// ```json + /// { + /// "message": "Forbidden", + /// "payload": { + /// "channels": [ + /// "test-channel1" + /// ], + /// "channel-groups": [ + /// "test-group1" + /// ] + /// }, + /// "error": true, + /// "service": "Access Manager", + /// "status": 403 + /// } + /// ``` + ErrorResponse(APIErrorBody), +} + +/// Content of successful subscribe REST API operation. +/// +/// Body contains next subscription cursor and list of receive real-time +/// updates. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +pub struct APISuccessBody { + /// Next subscription cursor. + /// + /// The cursor contains information about the start of the next real-time + /// update timeframe. + #[cfg_attr(feature = "serde", serde(rename = "t"))] + cursor: SubscribeCursor, + + /// List of updates. + /// + /// Contains list of real-time updates received using previous subscription + /// cursor. + #[cfg_attr(feature = "serde", serde(rename = "m"))] + messages: Vec, +} + +/// Single entry from subscribe response +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[allow(dead_code)] +pub(in crate::dx::subscribe) struct Envelope { + /// Shard number on which the event has been stored. + #[cfg_attr(feature = "serde", serde(rename = "a"))] + pub shard: String, + + /// A numeric representation of enabled debug flags. + #[cfg_attr(feature = "serde", serde(rename = "f"))] + pub debug_flags: u32, + + /// PubNub defined event type. + #[cfg_attr( + feature = "serde", + serde(rename = "f"), + serde(default = "Envelope::default_message_type") + )] + pub message_type: SubscribeMessageType, + + /// Identifier of client which sent message (set only when [`publish`] + /// called with `uuid`). + /// + /// [`publish`]: crate::dx::publish + #[cfg_attr(feature = "serde", serde(rename = "i"), serde(default))] + pub sender: Option, + + /// Sequence number (set only when [`publish`] called with `seqn`). + /// + /// [`publish`]: crate::dx::publish + #[cfg_attr(feature = "serde", serde(rename = "s"), serde(default))] + pub sequence_number: Option, + + /// Message "publish" time. + /// + /// This is the time when message has been received by [`PubNub`] network. + /// + /// [`PubNub`]: https://www.pubnub.com + #[cfg_attr(feature = "serde", serde(rename = "p"))] + pub published: SubscribeCursor, + + /// Name of channel where update received. + #[cfg_attr(feature = "serde", serde(rename = "c"))] + pub channel: String, + + /// Event payload. + /// + /// Depending from + #[cfg_attr(feature = "serde", serde(rename = "d"))] + pub payload: EnvelopePayload, + + /// Actual name of subscription through which event has been delivered. + /// + /// [`PubNubClientInstance`] can be used to subscribe to the group of + /// channels to receive updates and (group name will be set for field). + /// With this approach there will be no need to separately add *N* number of + /// channels to [`subscribe`] method call. + /// + /// [`subscribe`]: crate::dx::subscribe + #[cfg_attr(feature = "serde", serde(rename = "b"))] + pub subscription: String, + + /// User provided message type (set only when [`publish`] called with + /// `r#type`). + /// + /// [`publish`]: crate::dx::publish + #[cfg_attr(feature = "serde", serde(rename = "mt"), serde(default))] + pub r#type: Option, + + /// Identifier of space into which message has been published (set only when + /// [`publish`] called with `space_id`). + /// + /// [`publish`]: crate::dx::publish + #[cfg_attr(feature = "serde", serde(rename = "si"), serde(default))] + pub space_id: Option, +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[allow(dead_code)] +pub(in crate::dx::subscribe) enum EnvelopePayload { + /// Presence change real-time update. + /// + /// Payload represents one of the presence types: + /// * `join` – new user joined the channel + /// * `leave` – some user left channel + /// * `timeout` – service didn't notice user for a while + /// * `interval` – bulk update on `joined`, `left` and `timeout` users. + /// * `state-change` - some user changed state associated with him on channel. + Presence { + /// Presence event type. + action: Option, + + /// Unix timestamp when presence event has been triggered. + timestamp: usize, + + /// Unique identification of the user for whom the presence event has + /// been triggered. + uuid: Option, + + /// The current occupancy after the presence change is updated. + occupancy: Option, + + /// The user's state associated with the channel has been updated. + data: Option, + + /// The list of unique user identifiers that `joined` the channel since + /// the last interval presence update. + join: Option>, + + /// The list of unique user identifiers that `left` the channel since + /// the last interval presence update. + leave: Option>, + + /// The list of unique user identifiers that `timeout` the channel since + /// the last interval presence update. + timeout: Option>, + }, + /// Object realtime update. + Object { + /// The type of event that happened during the object update. + /// + /// Possible values are: + /// * `update` - object has been updated + /// * `delete` - object has been removed + event: String, + + /// Type of object for which update has been generated. + /// + /// Possible values are: + /// * `uuid` - update for user object + /// * `space` - update for channel object + /// * `membership` - update for user membership object + r#type: String, + + /// Information about object for which update has been generated. + data: ObjectDataBody, + + /// Name of service which generated update for object. + source: String, + + /// Version of service which generated update for object. + version: String, + }, + + MessageAction { + /// The type of event that happened during the message action update. + /// + /// Possible values are: + /// * `added` - action has been added to the message + /// * `removed` - action has been removed from message + event: String, + + /// Information about message action for which update has been + /// generated. + data: MessageActionDataBody, + + /// Name of service which generated update for message action. + source: String, + + /// Version of service which generated update for message action. + version: String, + }, + File { + /// Message which has been associated with uploaded file. + message: String, + + /// Information about uploaded file. + file: FileDataBody, + }, + + /// Real-time message update. + #[cfg(feature = "serde")] + Message(serde_json::Value), + + /// Real-time message update. + #[cfg(not(feature = "serde"))] + Message(Vec), +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[allow(dead_code)] +pub(in crate::dx::subscribe) enum ObjectDataBody { + /// `Channel` object update payload body. + Channel { + /// Given name of the channel object. + name: Option, + + /// `Channel` object additional description. + description: Option, + + /// `Channel` object type information. + r#type: Option, + + /// `Channel` object current status. + status: Option, + + /// Unique `channel` object identifier. + id: String, + + /// Flatten `HashMap` with additional information associated with + /// `channel` object. + custom: Option>, + + /// Recent `channel` object modification date. + updated: String, + + /// Current `channel` object state hash. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "eTag")))] + tag: String, + }, + + /// `Uuid` object update payload body. + Uuid { + /// Give `uuid` object name. + name: Option, + + /// Email address associated with `uuid` object. + email: Option, + + /// `uuid` object identifier in external systems. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "externalId")))] + external_id: Option, + + /// `uuid` object external profile URL. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "profileUrl")))] + profile_url: Option, + + /// `Uuid` object type information. + r#type: Option, + + /// `Uuid` object current status. + status: Option, + + /// Unique `uuid` object identifier. + id: String, + + /// Flatten `HashMap` with additional information associated with + /// `uuid` object. + custom: Option>, + + /// Recent `uuid` object modification date. + updated: String, + + /// Current `uuid` object state hash. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "eTag")))] + tag: String, + }, + + /// `Membership` object update payload body. + Membership { + /// `Channel` object within which `uuid` object registered as member. + channel: Box, + + /// Flatten `HashMap` with additional information associated with + /// `membership` object. + custom: Option>, + + /// Unique identifier of `uuid` object which has relationship with + /// `channel`. + uuid: String, + + /// `Membership` object current status. + status: Option, + + /// Recent `membership` object modification date. + updated: String, + + /// Current `membership` object state hash. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "eTag")))] + tag: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +pub(in crate::dx::subscribe) struct MessageActionDataBody { + /// Timetoken of message for which action has been added / removed. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "messageTimetoken")))] + pub message_timetoken: String, + + /// Timetoken of message action which has been added / removed. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "actionTimetoken")))] + pub action_timetoken: String, + + /// Message action type. + pub r#type: String, + + /// Value associated with message action `type`. + pub value: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +pub(in crate::dx::subscribe) struct FileDataBody { + /// Unique identifier of uploaded file. + pub id: String, + + /// Actual name with which file has been stored. + pub name: String, +} + +impl TryFrom for SubscribeResult { + type Error = PubNubError; + + fn try_from(value: SubscribeResponseBody) -> Result { + match value { + SubscribeResponseBody::SuccessResponse(resp) => { + let mut messages = Vec::new(); + for message in resp.messages { + messages.push(message.try_into()?) + } + + Ok(SubscribeResult { + cursor: resp.cursor, + messages, + }) + } + SubscribeResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +impl Envelope { + /// Default message type. + #[allow(dead_code)] + fn default_message_type() -> SubscribeMessageType { + SubscribeMessageType::Message + } +} + +impl Update { + /// Name of channel. + /// + /// Name of channel at which update has been received. + pub(crate) fn channel(&self) -> String { + match self { + Update::Presence(presence) => presence.channel(), + Update::Object(object) => object.channel(), + Update::MessageAction(action) => action.channel.to_string(), + Update::File(file) => file.channel.to_string(), + Update::Message(message) | Update::Signal(message) => message.channel.to_string(), + } + } + /// Name of channel. + /// + /// Name of channel at which update has been received. + pub(crate) fn channel_group(&self) -> String { + match self { + Update::Presence(presence) => presence.channel_group(), + Update::Object(object) => object.channel_group(), + Update::MessageAction(action) => action.subscription.to_string(), + Update::File(file) => file.subscription.to_string(), + Update::Message(message) | Update::Signal(message) => message.subscription.to_string(), + } + } +} + +impl TryFrom for Update { + type Error = PubNubError; + + fn try_from(value: Envelope) -> Result { + match value.payload { + EnvelopePayload::Presence { .. } => Ok(Update::Presence(value.try_into()?)), + EnvelopePayload::Object { .. } + if matches!(value.message_type, SubscribeMessageType::Object) => + { + Ok(Update::Object(value.try_into()?)) + } + EnvelopePayload::MessageAction { .. } + if matches!(value.message_type, SubscribeMessageType::MessageAction) => + { + Ok(Update::MessageAction(value.try_into()?)) + } + EnvelopePayload::File { .. } + if matches!(value.message_type, SubscribeMessageType::File) => + { + Ok(Update::File(value.try_into()?)) + } + EnvelopePayload::Message(_) => { + if matches!(value.message_type, SubscribeMessageType::Message) { + Ok(Update::Message(value.try_into()?)) + } else { + Ok(Update::Signal(value.try_into()?)) + } + } + _ => Err(PubNubError::Deserialization { + details: "Unable deserialize unknown payload".to_string(), + }), + } + } +} + +impl From for Vec { + #[cfg(feature = "serde")] + fn from(value: EnvelopePayload) -> Self { + if let EnvelopePayload::Message(payload) = value { + return serde_json::to_vec(&payload).unwrap_or_default(); + } + vec![] + } + + #[cfg(not(feature = "serde"))] + fn from(value: EnvelopePayload) -> Self { + if let EnvelopePayload::Message(payload) = value { + return payload; + } + vec![] + } +} diff --git a/src/dx/subscribe/subscription.rs b/src/dx/subscribe/subscription.rs deleted file mode 100644 index 5f5f9f07..00000000 --- a/src/dx/subscribe/subscription.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{ - core::{blocking::Transport, event_engine::EventEngine}, - dx::subscribe::event_engine::{ - effect_handler::{HandshakeFunction, ReceiveFunction, SubscribeEffectHandler}, - SubscribeState, - }, - lib::alloc::vec, - PubNubGenericClient, -}; - -use super::event_engine::{ - effect_handler::EmitFunction, SubscribeEffect, SubscribeEffectInvocation, -}; - -type SubscribeEngine = - EventEngine; - -/// 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. -/// -/// TODO: more description and examples -pub struct Subscription { - engine: SubscribeEngine, -} - -impl Subscription { - pub(crate) fn subscribe(_client: PubNubGenericClient) -> Self - where - T: Transport, - { - // TODO: implementation is a part of the different task - let handshake: HandshakeFunction = |_, _, _, _| Ok(vec![]); - - let receive: ReceiveFunction = |&_, &_, &_, _, _| Ok(vec![]); - - let emit: EmitFunction = |_| Ok(()); - - Self { - engine: SubscribeEngine::new( - SubscribeEffectHandler::new(handshake, receive, emit), - SubscribeState::Unsubscribed, - ), - } - } -} diff --git a/src/dx/subscribe/subscription_configuration.rs b/src/dx/subscribe/subscription_configuration.rs new file mode 100644 index 00000000..fffc83a6 --- /dev/null +++ b/src/dx/subscribe/subscription_configuration.rs @@ -0,0 +1,63 @@ +//! Subscriptions module configuration. +//! +//! This module contains [`SubscriptionConfiguration`] which allow user to +//! configure subscription module components. + +use crate::{ + core::Deserializer, + dx::subscribe::{result::SubscribeResponseBody, SubscriptionManager}, + lib::{ + alloc::sync::Arc, + core::{ + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, +}; + +/// Subscription module configuration. +#[derive(Debug)] +pub(crate) struct SubscriptionConfiguration { + pub(crate) inner: Arc, +} + +impl Deref for SubscriptionConfiguration { + type Target = SubscriptionConfigurationRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for SubscriptionConfiguration { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner).expect("Subscription stream is not unique") + } +} + +impl Clone for SubscriptionConfiguration { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +/// Subscription module configuration. +pub(crate) struct SubscriptionConfigurationRef { + /// Subscription manager + pub subscription_manager: SubscriptionManager, + + /// Received data deserializer. + pub deserializer: Option>>, +} + +impl Debug for SubscriptionConfigurationRef { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "SubscriptionConfiguration {{ manager: {:?} }}", + self.subscription_manager + ) + } +} diff --git a/src/dx/subscribe/subscription_manager.rs b/src/dx/subscribe/subscription_manager.rs new file mode 100644 index 00000000..e9d1d464 --- /dev/null +++ b/src/dx/subscribe/subscription_manager.rs @@ -0,0 +1,253 @@ +//! Subscriptions' manager. +//! +//! This module contains manager which is responsible for tracking and updating +//! active subscription streams. + +use super::event_engine::SubscribeEvent; +use crate::{ + dx::subscribe::{ + event_engine::SubscribeEventEngine, result::Update, subscription::Subscription, + SubscribeStatus, + }, + lib::alloc::{sync::Arc, vec::Vec}, +}; + +/// 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 +/// [`subscribe`] method call. +/// +/// [`subscription`]: crate::Subscription +/// [`PubNubClient`]: crate::PubNubClient +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct SubscriptionManager { + /// Subscription event engine. + /// + /// State machine which is responsible for subscription loop maintenance. + subscribe_event_engine: Arc, + + /// List of registered subscribers. + /// + /// List of subscribers which will receive real-time updates. + subscribers: Vec, +} + +#[allow(dead_code)] +impl SubscriptionManager { + pub fn new(subscribe_event_engine: Arc) -> Self { + Self { + subscribe_event_engine, + subscribers: Default::default(), + } + } + + pub fn notify_new_status(&self, status: &SubscribeStatus) { + self.subscribers.iter().for_each(|subscription| { + subscription.handle_status(status.clone()); + }); + } + + pub fn notify_new_messages(&self, messages: Vec) { + self.subscribers.iter().for_each(|subscription| { + subscription.handle_messages(&messages); + }); + } + + pub fn register(&mut self, subscription: Subscription) { + self.subscribers.push(subscription); + + self.change_subscription(); + } + + pub fn unregister(&mut self, subscription: Subscription) { + if let Some(position) = self + .subscribers + .iter() + .position(|val| val.id.eq(&subscription.id)) + { + self.subscribers.swap_remove(position); + } + + self.change_subscription(); + } + + fn change_subscription(&self) { + let channels = self + .subscribers + .iter() + .flat_map(|val| val.channels.iter()) + .cloned() + .collect::>(); + + let channel_groups = self + .subscribers + .iter() + .flat_map(|val| val.channel_groups.iter()) + .cloned() + .collect::>(); + + self.subscribe_event_engine + .process(&SubscribeEvent::SubscriptionChanged { + channels: (!channels.is_empty()).then_some(channels), + channel_groups: (!channel_groups.is_empty()).then_some(channel_groups), + }); + } +} + +#[cfg(test)] +mod should { + use super::*; + use crate::{ + core::RequestRetryPolicy, + dx::subscribe::{ + event_engine::{SubscribeEffectHandler, SubscribeState}, + result::SubscribeResult, + subscription::SubscriptionBuilder, + types::Message, + SubscriptionConfiguration, SubscriptionConfigurationRef, + }, + lib::alloc::sync::Arc, + providers::futures_tokio::TokioRuntime, + }; + use spin::RwLock; + + #[allow(dead_code)] + fn event_engine() -> Arc { + let (cancel_tx, _) = async_channel::bounded(1); + + SubscribeEventEngine::new( + SubscribeEffectHandler::new( + Arc::new(move |_| { + Box::pin(async move { + Ok(SubscribeResult { + cursor: Default::default(), + messages: Default::default(), + }) + }) + }), + Arc::new(|_| { + // Do nothing yet + }), + Arc::new(Box::new(|_| { + // Do nothing yet + })), + RequestRetryPolicy::None, + cancel_tx, + ), + SubscribeState::Unsubscribed, + TokioRuntime, + ) + } + + #[tokio::test] + async fn register_subscription() { + let mut manager = SubscriptionManager::new(event_engine()); + let dummy_manager = SubscriptionManager::new(event_engine()); + + let subscription = SubscriptionBuilder { + subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { + inner: Arc::new(SubscriptionConfigurationRef { + subscription_manager: dummy_manager, + deserializer: None, + }), + })))), + ..Default::default() + } + .channels(["test".into()]) + .execute() + .unwrap(); + + manager.register(subscription); + + assert_eq!(manager.subscribers.len(), 1); + } + + #[tokio::test] + async fn unregister_subscription() { + let mut manager = SubscriptionManager::new(event_engine()); + let dummy_manager = SubscriptionManager::new(event_engine()); + + let subscription = SubscriptionBuilder { + subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { + inner: Arc::new(SubscriptionConfigurationRef { + subscription_manager: dummy_manager, + deserializer: None, + }), + })))), + ..Default::default() + } + .channels(["test".into()]) + .execute() + .unwrap(); + + manager.register(subscription.clone()); + manager.unregister(subscription); + + assert_eq!(manager.subscribers.len(), 0); + } + + #[tokio::test] + async fn notify_subscription_about_statuses() { + let mut manager = SubscriptionManager::new(event_engine()); + let dummy_manager = SubscriptionManager::new(event_engine()); + + let subscription = SubscriptionBuilder { + subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { + inner: Arc::new(SubscriptionConfigurationRef { + subscription_manager: dummy_manager, + deserializer: None, + }), + })))), + ..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 + ); + } + + #[tokio::test] + async fn notify_subscription_about_updates() { + let mut manager = SubscriptionManager::new(event_engine()); + let dummy_manager = SubscriptionManager::new(event_engine()); + + let subscription = SubscriptionBuilder { + subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { + inner: Arc::new(SubscriptionConfigurationRef { + subscription_manager: dummy_manager, + deserializer: None, + }), + })))), + ..Default::default() + } + .channels(["test".into()]) + .execute() + .unwrap(); + + manager.register(subscription.clone()); + + manager.notify_new_messages(vec![Update::Message(Message { + channel: "test".into(), + ..Default::default() + })]); + + use futures::StreamExt; + assert!(subscription.message_stream().next().await.is_some()); + } +} diff --git a/src/dx/subscribe/types.rs b/src/dx/subscribe/types.rs index 79afa5a4..560146fe 100644 --- a/src/dx/subscribe/types.rs +++ b/src/dx/subscribe/types.rs @@ -1,26 +1,90 @@ //! Subscription types module. -use crate::lib::core::fmt::{Formatter, Result}; +use crate::{ + core::{Cryptor, PubNubError, ScalarValue}, + dx::subscribe::result::{Envelope, EnvelopePayload, ObjectDataBody, Update}, + lib::{ + alloc::{ + boxed::Box, + string::{String, ToString}, + sync::Arc, + vec::Vec, + }, + collections::HashMap, + core::{fmt::Formatter, result::Result}, + }, +}; +use base64::{engine::general_purpose, Engine}; + +#[derive(Debug, Clone)] +#[allow(dead_code, missing_docs)] +pub enum SubscribeStreamEvent { + Status(SubscribeStatus), + Update(Update), +} + +/// Known types of events / messages received from subscribe. +/// +/// While subscribed to channels and groups [`PubNub`] service may deliver +/// real-time updates which can be differentiated by their type. +/// This enum contains list of known general message types. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +pub enum SubscribeMessageType { + /// Regular messages. + /// + /// This type is set for events published by user using [`publish`] feature. + /// + /// [`publish`]: crate::dx::publish + Message = 0, + + /// Small message. + /// + /// Message sent with separate endpoint as chunk of really small data. + Signal = 1, + + /// Object related event. + /// + /// This type is set to the group of events which is related to the + /// `user Id` / `channel` objects and their relationship changes. + Object = 2, + + /// Message action related event. + /// + /// This type is set to the group of events which is related to the + /// `message` associated actions changes (addition, removal). + MessageAction = 3, + + /// File related event. + /// + /// This type is set to the group of events which is related to file + /// sharing (upload / removal). + File = 4, +} /// Time cursor. /// /// Cursor used by subscription loop to identify point in time after /// which updates will be delivered. -#[derive(Debug, Copy, Clone, PartialEq)] -#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] pub struct SubscribeCursor { /// PubNub high-precision timestamp. /// /// Aside of specifying exact time of receiving data / event this token used /// to catchup / follow on real-time updates. - pub timetoken: u64, + #[cfg_attr(feature = "serde", serde(rename = "t"))] + pub timetoken: String, /// Data center region for which `timetoken` has been generated. + #[cfg_attr(feature = "serde", serde(rename = "r"))] pub region: u32, } /// Subscription statuses. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum SubscribeStatus { /// Successfully connected and receiving real-time updates. Connected, @@ -31,14 +95,816 @@ pub enum SubscribeStatus { /// Real-time updates receive stopped. Disconnected, + + /// Connection attempt failed. + ConnectedError(PubNubError), +} + +/// Presence update information. +/// +/// Enum provides [`Presence::Join`], [`Presence::Leave`], [`Presence::Timeout`], +/// [`Presence::Interval`] and [`Presence::StateChange`] variants for updates +/// listener. These variants allow listener understand how presence changes on +/// channel. +#[derive(Debug, Clone)] +pub enum Presence { + /// Remote user `join` update. + /// + /// Contains information about the user which joined the channel. + Join { + /// Unix timestamp when the user joined the channel. + timestamp: usize, + + /// Unique identification of the user which joined the channel. + uuid: String, + + /// Name of channel to which user joined. + channel: String, + + /// Actual name of subscription through which `user joined` update has + /// been delivered. + subscription: String, + + /// Current channel occupancy after user joined. + occupancy: usize, + }, + + /// Remote user `leave` update. + /// + /// Contains information about the user which left the channel. + Leave { + /// Unix timestamp when the user left the channel. + timestamp: usize, + + /// Name of channel which user left. + channel: String, + + /// Actual name of subscription through which `user left` update has + /// been delivered. + subscription: String, + + /// Current channel occupancy after user left. + occupancy: usize, + + /// Unique identification of the user which left the channel. + uuid: String, + }, + + /// Remote user `timeout` update. + /// + /// Contains information about the user which unexpectedly left the channel. + Timeout { + /// Unix timestamp when event has been triggered. + timestamp: usize, + + /// Name of channel where user timeout. + channel: String, + + /// Actual name of subscription through which `user timeout` update has + /// been delivered. + subscription: String, + + /// Current channel occupancy after user timeout. + occupancy: usize, + + /// Unique identification of the user which timeout the channel. + uuid: String, + }, + + /// Channel `interval` presence update. + /// + /// Contains information about the users which joined / left / unexpectedly + /// left the channel since previous `interval` update. + Interval { + /// Unix timestamp when event has been triggered. + timestamp: usize, + + /// Name of channel where user timeout. + channel: String, + + /// Actual name of subscription through which `interval` update has been + /// delivered. + subscription: String, + + /// Current channel occupancy. + occupancy: usize, + + /// The list of unique user identifiers that `joined` the channel since + /// the last interval presence update. + join: Option>, + + /// The list of unique user identifiers that `left` the channel since + /// the last interval presence update. + leave: Option>, + + /// The list of unique user identifiers that `timeout` the channel since + /// the last interval presence update. + timeout: Option>, + }, + + /// Remote user `state` change update. + /// + /// Contains information about the user for which associated `state` has + /// been changed on `channel`. + StateChange { + /// Unix timestamp when event has been triggered. + timestamp: usize, + + /// Name of channel where user timeout. + channel: String, + + /// Actual name of subscription through which `state changed` update has + /// been delivered. + subscription: String, + + /// Unique identification of the user for which state has been changed. + uuid: String, + + /// The user's state associated with the channel has been updated. + data: Option, + }, +} + +/// Objects 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. +#[derive(Debug, Clone)] +pub enum Object { + /// `Channel` object update. + Channel { + /// The type of event that happened during the object update. + event: Option, + + /// Time when `channel` object has been updated. + timestamp: Option, + + /// Given name of the channel object. + name: Option, + + /// `Channel` object additional description. + description: Option, + + /// `Channel` object type information. + r#type: Option, + + /// `Channel` object current status. + status: Option, + + /// Unique `channel` object identifier. + id: String, + + /// Flatten `HashMap` with additional information associated with + /// `channel` object. + custom: Option>, + + /// Recent `channel` object modification date. + updated: String, + + /// Current `channel` object state hash. + tag: String, + + /// Actual name of subscription through which `channel object` update + /// has been delivered. + subscription: String, + }, + + /// `UUID` object update. + Uuid { + /// The type of event that happened during the object update. + event: Option, + + /// Time when `uuid` object has been updated. + timestamp: Option, + + /// Give `uuid` object name. + name: Option, + + /// Email address associated with `uuid` object. + email: Option, + + /// `uuid` object identifier in external systems. + external_id: Option, + + /// `uuid` object external profile URL. + profile_url: Option, + + /// `Uuid` object type information. + r#type: Option, + + /// `Uuid` object current status. + status: Option, + + /// Unique `uuid` object identifier. + id: String, + + /// Flatten `HashMap` with additional information associated with + /// `uuid` object. + custom: Option>, + + /// Recent `uuid` object modification date. + updated: String, + + /// Current `uuid` object state hash. + tag: String, + + /// Actual name of subscription through which `uuid object` update has + /// been delivered. + subscription: String, + }, + + /// `Membership` object update. + Membership { + /// The type of event that happened during the object update. + event: Option, + + /// Time when `membership` object has been updated. + timestamp: Option, + + /// `Channel` object within which `uuid` object registered as member. + channel: Box, + + /// Flatten `HashMap` with additional information associated with + /// `membership` object. + custom: Option>, + + /// `Membership` object current status. + status: Option, + + /// Unique identifier of `uuid` object which has relationship with + /// `channel`. + uuid: String, + + /// Recent `membership` object modification date. + updated: String, + + /// Current `membership` object state hash. + tag: String, + + /// Actual name of subscription through which `membership` update has + /// been delivered. + subscription: String, + }, +} + +/// Message information. +/// +/// [`Message`] type provides to the updates listener message's information. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Message { + /// Identifier of client which sent message / signal. + pub sender: Option, + + /// Time when message / signal has been published. + pub timestamp: usize, + + /// Name of channel where message / signal received. + pub channel: String, + + /// Actual name of subscription through which update has been delivered. + pub subscription: String, + + /// Data published along with message / signal. + pub data: Vec, + + /// User provided message type (set only when [`publish`] called with + /// `r#type`). + /// + /// [`publish`]: crate::dx::publish + pub r#type: Option, + + /// Identifier of space into which message has been published (set only when + /// [`publish`] called with `space_id`). + /// + /// [`publish`]: crate::dx::publish + pub space_id: Option, + + /// Decryption error details. + /// + /// Error is set when [`PubNubClient`] configured with cryptor and it wasn't + /// able to decrypt [`data`] in this message. + pub decryption_error: Option, +} + +/// Message's action update information. +/// +/// [`MessageAction`] type provides to the updates listener message's action +/// changes information. +#[derive(Debug, Clone)] +pub struct MessageAction { + /// The type of event that happened during the message action update. + pub event: MessageActionEvent, + + /// Identifier of client which sent updated message's actions. + pub sender: String, + + /// Time when message action has been changed. + pub timestamp: usize, + + /// Name of channel where update received. + pub channel: String, + + /// Actual name of subscription through which update has been delivered. + pub subscription: String, + + /// Timetoken of message for which action has been added / removed. + pub message_timetoken: String, + + /// Timetoken of message action which has been added / removed. + pub action_timetoken: String, + + /// Message action type. + pub r#type: String, + + /// Value associated with message action `type`. + pub value: String, +} + +/// File sharing information. +/// +/// [`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, + + /// Time when file has been shared. + pub timestamp: usize, + + /// Name of channel where file update received. + pub channel: String, + + /// Actual name of subscription through which update has been delivered. + pub subscription: String, + + /// Message which has been associated with uploaded file. + message: String, + + /// Unique identifier of uploaded file. + id: String, + + /// Actual name with which file has been stored. + name: String, +} + +/// Object update event types. +#[derive(Debug, Copy, Clone)] +pub enum ObjectEvent { + /// Object information has been modified. + Update, + + /// Object has been deleted. + Delete, +} + +/// Message's actions update event types. +#[derive(Debug, Copy, Clone)] +pub enum MessageActionEvent { + /// Message's action has been modified. + Update, + + /// Message's action has been deleted. + Delete, +} + +impl Default for SubscribeCursor { + fn default() -> Self { + Self { + timetoken: "0".into(), + region: 0, + } + } +} + +impl TryFrom for ObjectEvent { + type Error = PubNubError; + + fn try_from(value: String) -> Result { + match value.as_str() { + "update" => Ok(Self::Update), + "delete" => Ok(Self::Delete), + _ => Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected object event type".to_string(), + }), + } + } +} + +impl TryFrom for MessageActionEvent { + type Error = PubNubError; + + fn try_from(value: String) -> Result { + match value.as_str() { + "update" => Ok(Self::Update), + "delete" => Ok(Self::Delete), + _ => Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected message action event type".to_string(), + }), + } + } +} + +impl From for HashMap { + fn from(value: SubscribeCursor) -> Self { + if value.timetoken.eq(&"0") { + HashMap::from([("tt".into(), value.timetoken)]) + } else { + HashMap::from([ + ("tt".into(), value.timetoken.to_string()), + ("tr".into(), value.region.to_string()), + ]) + } + } } impl core::fmt::Display for SubscribeStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { + 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::ConnectedError(err) => write!(f, "ConnectionError({err:?})"), + } + } +} + +impl Presence { + /// Presence update channel. + /// + /// Name of channel at which presence update has been triggered. + pub(crate) fn channel(&self) -> String { + match self { + Presence::Join { channel, .. } + | Presence::Leave { channel, .. } + | Presence::Timeout { channel, .. } + | Presence::Interval { channel, .. } + | Presence::StateChange { channel, .. } => channel + .split('-') + .last() + .map(|name| name.to_string()) + .unwrap_or(channel.to_string()), + } + } + + /// Presence update channel group. + /// + /// Name of channel group through which presence update has been triggered. + pub(crate) fn channel_group(&self) -> String { + match self { + Presence::Join { subscription, .. } + | Presence::Leave { subscription, .. } + | Presence::Timeout { subscription, .. } + | Presence::Interval { subscription, .. } + | Presence::StateChange { subscription, .. } => subscription + .split('-') + .last() + .map(|name| name.to_string()) + .unwrap_or(subscription.to_string()), + } + } +} + +impl Object { + /// Object channel name. + /// + /// Name of channel (object id) at which object update has been triggered. + pub(crate) fn channel(&self) -> String { + match self { + Object::Channel { id, .. } | Object::Uuid { id, .. } => id.to_string(), + Object::Membership { uuid, .. } => uuid.to_string(), + } + } + + /// Object channel group name. + /// + /// Name of channel group through which object update has been triggered. + pub(crate) fn channel_group(&self) -> String { + match self { + Object::Channel { subscription, .. } + | Object::Uuid { subscription, .. } + | Object::Membership { subscription, .. } => subscription.to_string(), + } + } +} + +impl Update { + /// Decrypt real-time update. + pub(in crate::dx::subscribe) fn decrypt( + self, + cryptor: &Arc, + ) -> Self { + if !matches!(self, Self::Message(_) | Self::Signal(_)) { + return self; + } + + match self { + Self::Message(message) => Self::Message(message.decrypt(cryptor)), + Self::Signal(message) => Self::Signal(message.decrypt(cryptor)), + _ => unreachable!(), + } + } +} + +impl Message { + /// Decrypt message payload if possible. + fn decrypt(mut self, cryptor: &Arc) -> Self { + let lossy_string = String::from_utf8_lossy(self.data.as_slice()).to_string(); + let trimmed = lossy_string.trim_matches('"'); + let decryption_result = general_purpose::STANDARD + .decode(trimmed) + .map_err(|err| PubNubError::Decryption { + details: err.to_string(), + }) + .and_then(|base64_bytes| cryptor.decrypt(base64_bytes)); + + match decryption_result { + Ok(bytes) => { + self.data = bytes; + } + Err(error) => self.decryption_error = Some(error), + }; + + self + } +} + +impl TryFrom for Presence { + type Error = PubNubError; + + fn try_from(value: Envelope) -> Result { + if let EnvelopePayload::Presence { + action, + timestamp, + uuid, + occupancy, + data, + join, + leave, + timeout, + } = value.payload + { + let action = action.unwrap_or("interval".to_string()); + match action.as_str() { + "join" => Ok(Self::Join { + timestamp, + // `join` event always has `uuid` and unwrap_or default + // value won't be actually used. + uuid: uuid.unwrap_or("".to_string()), + channel: value.channel, + subscription: value.subscription, + occupancy: occupancy.unwrap_or(0), + }), + "leave" => Ok(Self::Leave { + timestamp, + // `leave` event always has `uuid` and unwrap_or default + // value won't be actually used. + uuid: uuid.unwrap_or("".to_string()), + channel: value.channel, + subscription: value.subscription, + occupancy: occupancy.unwrap_or(0), + }), + "timeout" => Ok(Self::Timeout { + timestamp, + // `leave` event always has `uuid` and unwrap_or default + // value won't be actually used. + uuid: uuid.unwrap_or("".to_string()), + channel: value.channel, + subscription: value.subscription, + occupancy: occupancy.unwrap_or(0), + }), + "interval" => Ok(Self::Interval { + timestamp, + channel: value.channel, + subscription: value.subscription, + occupancy: occupancy.unwrap_or(0), + join, + leave, + timeout, + }), + _ => Ok(Self::StateChange { + timestamp, + // `state-change` event always has `uuid` and unwrap_or + // default value won't be actually used. + uuid: uuid.unwrap_or("".to_string()), + channel: value.channel, + subscription: value.subscription, + data, + }), + } + } else { + Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected payload for presence.".to_string(), + }) + } + } +} + +impl TryFrom for Object { + type Error = PubNubError; + + fn try_from(value: Envelope) -> Result { + let timestamp = value.published.timetoken.parse::(); + if let EnvelopePayload::Object { + event, + r#type, + data, + .. + } = value.payload + { + let update_type = r#type; + match data { + ObjectDataBody::Channel { + name, + description, + r#type, + status, + id, + custom, + updated, + tag, + } if update_type.as_str().eq("channel") => Ok(Self::Channel { + event: Some(event.try_into()?), + timestamp: timestamp.ok(), + name, + description, + r#type, + status, + id, + custom, + updated, + tag, + subscription: value.subscription, + }), + ObjectDataBody::Uuid { + name, + email, + external_id, + profile_url, + r#type, + status, + id, + custom, + updated, + tag, + } if update_type.as_str().eq("uuid") => Ok(Self::Uuid { + event: Some(event.try_into()?), + timestamp: timestamp.ok(), + name, + email, + external_id, + profile_url, + r#type, + status, + id, + custom, + updated, + tag, + subscription: value.subscription, + }), + ObjectDataBody::Membership { + channel, + custom, + uuid, + status, + updated, + tag, + } if update_type.as_str().eq("membership") => { + if let ObjectDataBody::Channel { + name, + description: channel_description, + r#type: channel_type, + status: channel_status, + id, + custom: channel_custom, + updated: channel_updated, + tag: channel_tag, + } = *channel + { + Ok(Self::Membership { + event: Some(event.try_into()?), + timestamp: timestamp.ok(), + channel: Box::new(Object::Channel { + event: None, + timestamp: None, + name, + description: channel_description, + r#type: channel_type, + status: channel_status, + id, + custom: channel_custom, + updated: channel_updated, + tag: channel_tag, + subscription: value.subscription.clone(), + }), + custom, + status, + uuid, + updated, + tag, + subscription: value.subscription, + }) + } else { + Err(PubNubError::Deserialization { + details: "Unable deserialize: unknown object type.".to_string(), + }) + } + } + _ => Err(PubNubError::Deserialization { + details: "Unable deserialize: unknown object type.".to_string(), + }), + } + } else { + Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected payload for object.".to_string(), + }) + } + } +} + +impl TryFrom for Message { + type Error = PubNubError; + + fn try_from(value: Envelope) -> Result { + // `Message` / `signal` always has `timetoken` and unwrap_or default + // value won't be actually used. + let timestamp = value.published.timetoken.parse::().ok().unwrap_or(0); + + if let EnvelopePayload::Message(_) = value.payload { + Ok(Self { + sender: value.sender, + timestamp, + channel: value.channel, + subscription: value.subscription, + data: value.payload.into(), + r#type: value.r#type, + space_id: value.space_id, + decryption_error: None, + }) + } else { + Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected payload for message.".to_string(), + }) + } + } +} + +impl TryFrom for MessageAction { + type Error = PubNubError; + + fn try_from(value: Envelope) -> Result { + // `Message action` event always has `timetoken` and unwrap_or default + // value won't be actually used. + let timestamp = value.published.timetoken.parse::().ok().unwrap_or(0); + // `Message action` event always has `sender` and unwrap_or default + // value won't be actually used. + let sender = value.sender.unwrap_or("".to_string()); + if let EnvelopePayload::MessageAction { event, data, .. } = value.payload { + Ok(Self { + event: event.try_into()?, + sender, + timestamp, + channel: value.channel, + subscription: value.subscription, + message_timetoken: data.message_timetoken, + action_timetoken: data.action_timetoken, + r#type: data.r#type, + value: data.value, + }) + } else { + Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected payload for message action.".to_string(), + }) + } + } +} + +impl TryFrom for File { + type Error = PubNubError; + + fn try_from(value: Envelope) -> Result { + // `File` event always has `timetoken` and unwrap_or default + // value won't be actually used. + let timestamp = value.published.timetoken.parse::().ok().unwrap_or(0); + // `File` event always has `sender` and unwrap_or default + // value won't be actually used. + let sender = value.sender.unwrap_or("".to_string()); + if let EnvelopePayload::File { message, file } = value.payload { + Ok(Self { + sender, + timestamp, + channel: value.channel, + subscription: value.subscription, + message, + id: file.id, + name: file.name, + }) + } else { + Err(PubNubError::Deserialization { + details: "Unable deserialize: unexpected payload for file.".to_string(), + }) } } } diff --git a/src/lib.rs b/src/lib.rs index fc286d59..c1a0b013 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,41 +225,6 @@ mod lib { pub(crate) use std::collections::HashMap; } } - - #[cfg(any(feature = "publish", feature = "access"))] - pub(crate) mod encoding { - use super::alloc::string::{String, ToString}; - use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; - - /// https://url.spec.whatwg.org/#fragment-percent-encode-set - const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); - - /// https://url.spec.whatwg.org/#path-percent-encode-set - const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); - - /// https://url.spec.whatwg.org/#userinfo-percent-encode-set - pub(crate) const USERINFO: &AsciiSet = &PATH - .add(b'/') - .add(b':') - .add(b';') - .add(b'=') - .add(b'@') - .add(b'[') - .add(b'\\') - .add(b']') - .add(b'^') - .add(b'|'); - - /// `+` sign needed by PubNub API - pub(crate) const PUBNUB_SET: &AsciiSet = &USERINFO.add(b'+'); - - /// `percent_encoding` crate recommends you to create your own set for encoding. - /// To be consistent in the whole codebase - we created a function that can be used - /// for encoding related stuff. - pub(crate) fn url_encode(data: &[u8]) -> String { - percent_encode(data, PUBNUB_SET).to_string() - } - } } // Mocking random for checking if `no_std` compiles. diff --git a/src/providers/crypto_aescbc.rs b/src/providers/crypto_aescbc.rs index f39b439b..59c12120 100644 --- a/src/providers/crypto_aescbc.rs +++ b/src/providers/crypto_aescbc.rs @@ -71,6 +71,7 @@ pub enum AesCbcIv { /// # Ok(()) /// # } /// ``` +#[derive(Debug)] pub struct AesCbcCrypto { cipher_key: Vec, iv: Option>, @@ -117,7 +118,7 @@ impl AesCbcCrypto { fn estimated_enc_buffer_size(&self, source: &[u8]) -> usize { // Adding padding which include additional AES cipher block size. let padding = (AES_BLOCK_SIZE - source.len() % AES_BLOCK_SIZE) + AES_BLOCK_SIZE; - if !self.iv_constant { + if !&self.iv_constant { // Reserve more space to store random initialization vector. source.len() + padding + AES_BLOCK_SIZE } else { @@ -131,7 +132,12 @@ impl AesCbcCrypto { /// type of used initialization vector. fn estimated_dec_buffer_size(&self, source: &[u8]) -> usize { // Subtract size of random initialization vector (if used). - source.len() - if !self.iv_constant { AES_BLOCK_SIZE } else { 0 } + source.len() + - if !&self.iv_constant { + AES_BLOCK_SIZE + } else { + 0 + } } /// Data encryption initialization vector. @@ -197,7 +203,7 @@ impl Cryptor for AesCbcCrypto { /// # fn main() -> Result<(), PubNubError> { /// let cryptor = // AesCbcCrypto /// # AesCbcCrypto::new("enigma", AesCbcIv::Random)?; - /// let encrypted_data = cryptor.encrypt("Hello world!".as_bytes()); + /// let encrypted_data = cryptor.encrypt(Vec::from("Hello world!")); /// match encrypted_data { /// Ok(data) => println!("Encrypted data: {:?}", data), /// Err(err) => eprintln!("Data encryption error: {}", err.to_string()) @@ -209,14 +215,15 @@ impl Cryptor for AesCbcCrypto { /// # Errors /// Should return an [`PubNubError::Encryption`] if provided data can't /// be encrypted or underlying cryptor misconfigured. - fn encrypt<'en, T>(&self, source: T) -> Result, PubNubError> - where - T: Into<&'en [u8]>, - { + fn encrypt(&self, source: Vec) -> Result, PubNubError> { let iv = self.encryption_iv(); - let data = source.into(); + let data = source.as_slice(); let mut buffer = vec![0u8; self.estimated_enc_buffer_size(data)]; - let data_offset = if !self.iv_constant { AES_BLOCK_SIZE } else { 0 }; + let data_offset = if !&self.iv_constant { + AES_BLOCK_SIZE + } else { + 0 + }; let data_slice = &mut buffer[data_offset..]; let result = Encryptor::new(self.cipher_key.as_slice().into(), iv.as_slice().into()) @@ -254,7 +261,7 @@ impl Cryptor for AesCbcCrypto { /// # .decode("fRm/rMArHgQuIuhuJMbXV8JLOUqf5sP72lGC4EaW98nNhmJltQcmCol9XXWgeDJC") /// # .expect("Valid base64 encoded string required."); /// let encrypted_data = // &[u8] - /// # data_for_decryption.as_slice(); + /// # data_for_decryption; /// let decrypted_data = cryptor.decrypt(encrypted_data); /// match decrypted_data { /// Ok(data) => println!("Decrypted data: {:?}", String::from_utf8(data)), // "Hello there 🙃" @@ -267,14 +274,15 @@ impl Cryptor for AesCbcCrypto { /// # Errors /// Should return an [`PubNubError::Decryption`] if provided data can't /// be decrypted or underlying cryptor misconfigured. - fn decrypt<'de, T>(&self, source: T) -> Result, PubNubError> - where - T: Into<&'de [u8]>, - { - let data = source.into(); + fn decrypt(&self, source: Vec) -> Result, PubNubError> { + let data = source.as_slice(); let iv = self.decryption_iv(data); let mut buffer = vec![0u8; self.estimated_dec_buffer_size(data)]; - let data_offset = if !self.iv_constant { AES_BLOCK_SIZE } else { 0 }; + let data_offset = if !&self.iv_constant { + AES_BLOCK_SIZE + } else { + 0 + }; let data_slice = &data[data_offset..]; let result = Decryptor::new(self.cipher_key.as_slice().into(), iv.as_slice().into()) @@ -327,10 +335,10 @@ mod it_should { let cryptor = AesCbcCrypto::new("enigma", AesCbcIv::Constant).expect("Cryptor should be created"); let encrypted1 = cryptor - .encrypt("\"Hello there 🙃\"".as_bytes()) + .encrypt(Vec::from("\"Hello there 🙃\"")) .expect("Data should be encrypted"); let encrypted2 = cryptor - .encrypt("\"Hello there 🙃\"".as_bytes()) + .encrypt(Vec::from("\"Hello there 🙃\"")) .expect("Data should be encrypted"); assert_eq!(encrypted1, encrypted2); assert_ne!( @@ -348,10 +356,10 @@ mod it_should { let cryptor = AesCbcCrypto::new("enigma", AesCbcIv::Random).expect("Cryptor should be created"); let encrypted1 = cryptor - .encrypt("\"Hello there 🙃\"".as_bytes()) + .encrypt(Vec::from("\"Hello there 🙃\"")) .expect("Data should be encrypted"); let encrypted2 = cryptor - .encrypt("\"Hello there 🙃\"".as_bytes()) + .encrypt(Vec::from("\"Hello there 🙃\"")) .expect("Data should be encrypted"); assert_ne!(encrypted1, encrypted2); assert_ne!(encrypted1[0..AES_BLOCK_SIZE], encrypted2[0..AES_BLOCK_SIZE]); @@ -365,7 +373,7 @@ mod it_should { let cryptor = AesCbcCrypto::new("enigma", AesCbcIv::Constant).expect("Cryptor should be created"); let decrypted = cryptor - .decrypt(encrypted.as_slice()) + .decrypt(encrypted) .expect("Data should be decrypted"); assert_eq!(decrypted, "\"Hello there 🙃\"".as_bytes()); } @@ -381,10 +389,10 @@ mod it_should { let cryptor = AesCbcCrypto::new("enigma", AesCbcIv::Random).expect("Cryptor should be created"); let decrypted1 = cryptor - .decrypt(encrypted1.as_slice()) + .decrypt(encrypted1) .expect("Data should be decrypted"); let decrypted2 = cryptor - .decrypt(encrypted2.as_slice()) + .decrypt(encrypted2) .expect("Data should be decrypted"); assert_eq!(decrypted1, "\"Hello there 🙃\"".as_bytes()); assert_eq!(decrypted1, decrypted2); diff --git a/src/providers/deserialization_serde.rs b/src/providers/deserialization_serde.rs index 99619033..cbbbe4f2 100644 --- a/src/providers/deserialization_serde.rs +++ b/src/providers/deserialization_serde.rs @@ -34,11 +34,25 @@ use crate::{ /// [`dx`]: ../dx/index.html pub struct SerdeDeserializer; -impl<'de, T> Deserializer<'de, T> for SerdeDeserializer +impl Deserializer for SerdeDeserializer where - T: serde::Deserialize<'de>, + T: for<'de> serde::Deserialize<'de>, { - fn deserialize(&self, bytes: &'de [u8]) -> Result { + fn deserialize(&self, bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(|e| PubNubError::Deserialization { + details: e.to_string(), + }) + } +} + +impl<'de, D> crate::core::Deserialize<'de> for D +where + D: Send + Sync, + D: serde::Deserialize<'de>, +{ + type Type = D; + + fn deserialize(bytes: &'de [u8]) -> Result { serde_json::from_slice(bytes).map_err(|e| PubNubError::Deserialization { details: e.to_string(), }) @@ -60,7 +74,7 @@ mod should { fn deserialize() { let sut = SerdeDeserializer; - let result: Foo = sut.deserialize(b"{\"bar\":\"baz\"}").unwrap(); + let result: Foo = sut.deserialize(&Vec::from("{\"bar\":\"baz\"}")).unwrap(); assert_eq!( result, diff --git a/src/providers/futures_tokio.rs b/src/providers/futures_tokio.rs new file mode 100644 index 00000000..6b6a0bc3 --- /dev/null +++ b/src/providers/futures_tokio.rs @@ -0,0 +1,27 @@ +//! # Futures implementation using Tokio runtime +//! +//! This module contains [`TokioSpawner`] type. +//! +//! It requires the [`future_tokio` feature] to be enabled. +//! +//! [`future_tokio` feature]: ../index.html#features + +use crate::{core::runtime::Runtime, lib::alloc::boxed::Box}; + +/// Tokio-based `async` tasks spawner. +#[derive(Copy, Clone, Debug)] +pub struct TokioRuntime; + +#[async_trait::async_trait] +impl Runtime for TokioRuntime { + fn spawn(&self, future: impl futures::Future + Send + 'static) + where + R: Send + 'static, + { + tokio::spawn(future); + } + + async fn sleep(self, delay: u64) { + tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await + } +} diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 24357da8..cd0f6891 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -14,3 +14,6 @@ pub mod deserialization_serde; #[cfg(feature = "aescbc")] pub mod crypto_aescbc; + +#[cfg(feature = "futures_tokio")] +pub mod futures_tokio; diff --git a/src/transport/middleware.rs b/src/transport/middleware.rs index 9ed915c9..d8222bac 100644 --- a/src/transport/middleware.rs +++ b/src/transport/middleware.rs @@ -5,12 +5,12 @@ #[cfg(feature = "std")] use crate::{ - core::TransportMethod, - lib::{alloc::vec::Vec, collections::HashMap, encoding::url_encode}, + core::{utils::encoding::url_encode, TransportMethod}, + lib::{alloc::vec::Vec, collections::HashMap}, }; use crate::{ core::{ - metadata::{PKG_VERSION, RUSTC_VERSION, SDK_ID, TARGET}, + utils::metadata::{PKG_VERSION, RUSTC_VERSION, SDK_ID, TARGET}, PubNubError, Transport, TransportRequest, TransportResponse, }, lib::{ @@ -229,7 +229,7 @@ mod should { } let middleware = PubNubMiddleware { - transport: MockTransport::default(), + transport: MockTransport, instance_id: Arc::new(Some(String::from("instance_id"))), user_id: String::from("user_id").into(), signature_keys: None, @@ -296,7 +296,7 @@ mod should { } let middleware = PubNubMiddleware { - transport: MockTransport::default(), + transport: MockTransport, instance_id: Some(String::from("instance_id")).into(), user_id: "user_id".to_string().into(), signature_keys: None, diff --git a/src/transport/reqwest.rs b/src/transport/reqwest.rs index fe3ebbf9..54ef6c83 100644 --- a/src/transport/reqwest.rs +++ b/src/transport/reqwest.rs @@ -14,8 +14,8 @@ use crate::{ core::{ - error::PubNubError, transport::PUBNUB_DEFAULT_BASE_URL, Transport, TransportMethod, - TransportRequest, TransportResponse, + error::PubNubError, transport::PUBNUB_DEFAULT_BASE_URL, utils::encoding::url_encode, + Transport, TransportMethod, TransportRequest, TransportResponse, }, lib::{ alloc::{ @@ -24,7 +24,6 @@ use crate::{ string::{String, ToString}, }, collections::HashMap, - encoding::url_encode, }, PubNubClientBuilder, }; @@ -86,6 +85,7 @@ impl Transport for TransportReqwest { .await .map_err(|e| PubNubError::Transport { details: e.to_string(), + response: None, })?; let headers = result.headers().clone(); @@ -95,6 +95,11 @@ impl Transport for TransportReqwest { .await .map_err(|e| PubNubError::Transport { details: e.to_string(), + response: Some(Box::new(TransportResponse { + status: status.into(), + headers: extract_headers(&headers), + body: None, + })), }) .and_then(|bytes| create_result(status, bytes, &headers)) } @@ -159,6 +164,7 @@ impl TransportReqwest { .body .ok_or(PubNubError::Transport { details: "Body should not be empty for POST".into(), + response: None, }) .map(|vec_bytes| self.reqwest_client.post(url).body(vec_bytes)) } @@ -179,10 +185,12 @@ fn prepare_headers(request_headers: &HashMap) -> Result HashMap { + headers + .iter() + .fold(HashMap::new(), |mut acc, (name, value)| { + if let Ok(value) = value.to_str() { + acc.insert(name.to_string(), value.to_string()); + } + acc + }) +} + fn create_result( status: StatusCode, body: Bytes, headers: &HeaderMap, ) -> Result { - let headers: HashMap = - headers - .iter() - .fold(HashMap::new(), |mut acc, (name, value)| { - if let Ok(value) = value.to_str() { - acc.insert(name.to_string(), value.to_string()); - } - acc - }); - Ok(TransportResponse { status: status.as_u16(), body: (!body.is_empty()).then(|| body.to_vec()), - headers, + headers: extract_headers(headers), }) } @@ -274,12 +283,16 @@ pub mod blocking { use log::info; + use crate::transport::reqwest::extract_headers; use crate::{ core::{ transport::PUBNUB_DEFAULT_BASE_URL, PubNubError, TransportMethod, TransportRequest, TransportResponse, }, - lib::alloc::string::{String, ToString}, + lib::alloc::{ + boxed::Box, + string::{String, ToString}, + }, transport::reqwest::{create_result, prepare_headers, prepare_url}, PubNubClientBuilder, }; @@ -332,6 +345,7 @@ pub mod blocking { .send() .map_err(|e| PubNubError::Transport { details: e.to_string(), + response: None, })?; let headers = result.headers().clone(); @@ -340,6 +354,11 @@ pub mod blocking { .bytes() .map_err(|e| PubNubError::Transport { details: e.to_string(), + response: Some(Box::new(TransportResponse { + status: status.into(), + headers: extract_headers(&headers), + body: None, + })), }) .and_then(|bytes| create_result(status, bytes, &headers)) } @@ -441,9 +460,8 @@ pub mod blocking { #[cfg(test)] mod should { - use crate::core::blocking::Transport; - use super::*; + use crate::{core::blocking::Transport, lib::alloc::string::ToString}; use test_case::test_case; use wiremock::matchers::{body_string, method, path as path_macher}; @@ -533,6 +551,8 @@ pub mod blocking { #[cfg(test)] mod should { use super::*; + use crate::lib::alloc::string::ToString; + use test_case::test_case; use wiremock::matchers::{body_string, header, method, path as path_macher}; use wiremock::{Mock, MockServer, ResponseTemplate}; diff --git a/tests/common/common_steps.rs b/tests/common/common_steps.rs index b12aa3c6..bfbc371a 100644 --- a/tests/common/common_steps.rs +++ b/tests/common/common_steps.rs @@ -1,4 +1,7 @@ +use cucumber::gherkin::Scenario; use cucumber::{given, then, World}; +use pubnub::core::RequestRetryPolicy; +use pubnub::dx::subscribe::subscription::Subscription; use pubnub::{ core::PubNubError, dx::{ @@ -88,9 +91,11 @@ impl Default for PAMState { Self { revoke_token_result: Err(PubNubError::Transport { details: "This is default value".into(), + response: None, }), grant_token_result: Err(PubNubError::Transport { details: "This is default value".into(), + response: None, }), resource_type: PAMCurrentResourceType::default(), resource_permissions: PAMPermissions::default(), @@ -105,8 +110,11 @@ impl Default for PAMState { #[derive(Debug, World)] pub struct PubNubWorld { + pub scenario: Option, pub keyset: pubnub::Keyset, pub publish_result: Result, + pub subscription: Result, + pub retry_policy: Option, pub pam_state: PAMState, pub api_error: Option, pub is_succeed: bool, @@ -115,6 +123,7 @@ pub struct PubNubWorld { impl Default for PubNubWorld { fn default() -> Self { PubNubWorld { + scenario: None, keyset: Keyset:: { subscribe_key: "demo".to_owned(), publish_key: Some("demo".to_string()), @@ -122,10 +131,16 @@ impl Default for PubNubWorld { }, publish_result: Err(PubNubError::Transport { details: "This is default value".into(), + response: None, + }), + subscription: Err(PubNubError::Transport { + details: "This is default value".into(), + response: None, }), is_succeed: false, pam_state: PAMState::default(), api_error: None, + retry_policy: None, } } } @@ -137,18 +152,37 @@ impl PubNubWorld { transport.hostname = "http://localhost:8090".into(); transport }; - PubNubClientBuilder::with_transport(transport) + + let mut builder = PubNubClientBuilder::with_transport(transport) .with_keyset(keyset) - .with_user_id("test") - .build() - .unwrap() + .with_user_id("test"); + + if let Some(retry_policy) = &self.retry_policy { + builder = builder.with_retry_policy(retry_policy.clone()); + } + + builder.build().unwrap() } } #[given("the demo keyset")] +#[given("the demo keyset with event engine enabled")] fn set_keyset(world: &mut PubNubWorld) { - world.keyset.publish_key = Some("demo".to_string()); - world.keyset.subscribe_key = "demo".to_string(); + world.keyset = Keyset { + subscribe_key: "demo".into(), + publish_key: Some("demo".into()), + secret_key: None, + } +} + +#[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 { + max_retry, + delay: 0, + }) + } } #[given(regex = r"^I have a keyset with access manager enabled(.*)?")] @@ -198,7 +232,7 @@ fn an_auth_error_is_returned(world: &mut PubNubWorld) { #[then(regex = r"^the error status code is (\d+)$")] #[given(regex = r"^the error status code is (\d+)$")] fn has_specific_error_code(world: &mut PubNubWorld, expected_status_code: u16) { - if let PubNubError::API { status, .. } = world.api_error.clone().unwrap() { + if let Some(PubNubError::API { status, .. }) = world.api_error.clone() { assert_eq!(status, expected_status_code); } else { panic!("API error is missing"); diff --git a/tests/contract_test.rs b/tests/contract_test.rs index 6a55e4f7..33b3af45 100644 --- a/tests/contract_test.rs +++ b/tests/contract_test.rs @@ -1,9 +1,11 @@ use cucumber::{writer, World, WriterExt}; -use std::fs::File; +use std::fs::{create_dir_all, read_to_string, File, OpenOptions}; +use std::process; mod access; mod common; mod publish; +mod subscribe; use common::PubNubWorld; async fn init_server(script: String) -> Result> { @@ -21,7 +23,7 @@ fn get_feature_set(tags: &[String]) -> String { } fn feature_allows_beta(feature: &str) -> bool { - let features: Vec<&str> = vec!["access", "publish"]; + let features: Vec<&str> = vec!["access", "publish", "eventEngine"]; features.contains(&feature) } @@ -36,7 +38,7 @@ fn feature_allows_contract_less(feature: &str) -> bool { } fn is_ignored_feature_set_tag(feature: &str, tags: &[String]) -> bool { - let supported_features = ["access", "publish"]; + let supported_features = ["access", "publish", "eventEngine"]; let mut ignored_tags = vec!["na=rust"]; if !feature_allows_beta(feature) { @@ -66,14 +68,46 @@ fn is_ignored_scenario_tag(feature: &str, tags: &[String]) -> bool { .any(|tag| tag.starts_with(format!("contract={tested_contract}").as_str())) } +pub fn scenario_name(world: &mut PubNubWorld) -> String { + world.scenario.as_ref().unwrap().name.clone() +} + +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"); + } +} + +fn logger_target() -> env_logger::Target { + create_dir_all("tests/logs").expect("Unable to create required directories for logs"); + env_logger::Target::Pipe(Box::new( + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open("tests/logs/log.txt") + .expect("Unable to open log file"), + )) +} + #[tokio::main] async fn main() { - env_logger::builder().try_init().unwrap(); + env_logger::builder() + .target(logger_target()) + .try_init() + .unwrap(); let _ = std::fs::create_dir_all("tests/reports"); let file: File = File::create("tests/reports/report-required.xml").unwrap(); PubNubWorld::cucumber() .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| { + .before(|_feature, _rule, scenario, world| { + world.scenario = Some(scenario.clone()); + futures::FutureExt::boxed(async move { if scenario.tags.iter().any(|t| t.starts_with("contract=")) { let tag = scenario @@ -104,4 +138,10 @@ async fn main() { || is_ignored_scenario_tag(¤t_feature, &scenario.tags)) }) .await; + + let report = + read_to_string("tests/reports/report-required.xml").expect("Unable to load reports"); + if report.contains("✘") { + process::exit(1) + } } diff --git a/tests/subscribe/mod.rs b/tests/subscribe/mod.rs new file mode 100644 index 00000000..c0191af7 --- /dev/null +++ b/tests/subscribe/mod.rs @@ -0,0 +1 @@ +pub mod subscribe_steps; diff --git a/tests/subscribe/subscribe_steps.rs b/tests/subscribe/subscribe_steps.rs new file mode 100644 index 00000000..1e19a638 --- /dev/null +++ b/tests/subscribe/subscribe_steps.rs @@ -0,0 +1,175 @@ +use crate::common::PubNubWorld; +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 std::fs::read_to_string; + +/// 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(); + + for line in written_log.lines() { + if !line.contains(" DEBUG ") { + continue; + } + + if let Some(matched) = event_regex.captures(line) { + let (_, [captured]) = matched.extract(); + 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()); + } + } + + 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("I subscribe")] +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(); +} + +#[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(); +} + +#[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(); + + select_biased! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).fuse() => panic!("No service response"), + _ = subscription.next().fuse() => println!("Message received from server") + } +} + +#[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(); + + select_biased! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(1)).fuse() => log::debug!("One \ + second is done"), + _ = subscription.next().fuse() => panic!("Message update from server") + } + + println!("~~~> {:?}", &world.retry_policy); + + let expected_retry_count: usize = usize::from(match &world.retry_policy.clone().unwrap() { + RequestRetryPolicy::Linear { max_retry, .. } + | RequestRetryPolicy::Exponential { max_retry, .. } => *max_retry, + _ => 0, + }); + + let handshake_test = scenario_name(world).to_lowercase().contains("handshake"); + let history = events_and_invocations_history(); + let normal_operation_name = { + if handshake_test { + "HANDSHAKE_FAILURE" + } else { + "RECEIVE_FAILURE" + } + }; + let reconnect_operation_name = { + if handshake_test { + "HANDSHAKE_RECONNECT_FAILURE" + } else { + "RECEIVE_RECONNECT_FAILURE" + } + }; + let give_up_operation_name = { + if handshake_test { + "HANDSHAKE_RECONNECT_GIVEUP" + } else { + "RECEIVE_RECONNECT_GIVEUP" + } + }; + + assert_eq!( + event_occurrence_count(history.clone(), normal_operation_name.into()), + 1 + ); + assert_eq!( + event_occurrence_count(history.clone(), reconnect_operation_name.into()), + expected_retry_count + ); + assert_eq!( + event_occurrence_count(history, give_up_operation_name.into()), + 1 + ); +} + +#[then("I observe the following:")] +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.") + } +}