flowfull 0.1.0

Async Rust client for Flowfull and Flowless-compatible backends
Documentation
use std::sync::Arc;

use serde::{Serialize, de::DeserializeOwned};
use tokio::sync::RwLock;

use crate::{FlowfullError, Result, config::ClientConfig, storage::Storage};

pub const PUBFLOW_SESSION_ID: &str = "pubflow_session_id";
pub const PUBFLOW_USER_DATA: &str = "pubflow_user_data";

const LEGACY_SESSION_KEYS: &[&str] = &["flowfull_session_id", "session_id", "sessionId"];

#[derive(Clone)]
pub struct SessionManager {
    config: Arc<ClientConfig>,
    storage: Option<Arc<dyn Storage>>,
    cached_session_id: Arc<RwLock<Option<String>>>,
}

impl SessionManager {
    pub fn new(config: Arc<ClientConfig>) -> Self {
        Self {
            storage: config.storage.clone(),
            config,
            cached_session_id: Arc::new(RwLock::new(None)),
        }
    }

    pub async fn get_session_id(&self) -> Result<Option<String>> {
        if let Some(cached) = self.cached_session_id.read().await.clone() {
            return Ok(Some(cached));
        }

        if let Some(session_id) = &self.config.session_id {
            self.cache_session_id(session_id.clone()).await;
            return Ok(Some(session_id.clone()));
        }

        if let Some(provider) = &self.config.session_provider {
            if let Some(session_id) = provider().await? {
                if !session_id.is_empty() {
                    self.cache_session_id(session_id.clone()).await;
                    return Ok(Some(session_id));
                }
            }
        }

        if let Some(storage) = &self.storage {
            if let Some(session_id) = storage.get_item(PUBFLOW_SESSION_ID).await? {
                if !session_id.is_empty() {
                    self.cache_session_id(session_id.clone()).await;
                    return Ok(Some(session_id));
                }
            }

            for key in LEGACY_SESSION_KEYS {
                if let Some(session_id) = storage.get_item(key).await? {
                    if !session_id.is_empty() {
                        self.cache_session_id(session_id.clone()).await;
                        return Ok(Some(session_id));
                    }
                }
            }
        }

        Ok(None)
    }

    pub async fn set_session_id(&self, session_id: impl Into<String>) -> Result<()> {
        let session_id = session_id.into();
        self.cache_session_id(session_id.clone()).await;
        if let Some(storage) = &self.storage {
            storage.set_item(PUBFLOW_SESSION_ID, &session_id).await?;
        }
        Ok(())
    }

    pub async fn clear_session(&self) -> Result<()> {
        *self.cached_session_id.write().await = None;
        if let Some(storage) = &self.storage {
            storage.remove_item(PUBFLOW_SESSION_ID).await?;
        }
        Ok(())
    }

    pub async fn has_session(&self) -> Result<bool> {
        Ok(self.get_session_id().await?.is_some())
    }

    pub async fn set_user_data<T>(&self, user: &T) -> Result<()>
    where
        T: Serialize + ?Sized,
    {
        let storage = self
            .storage
            .as_ref()
            .ok_or_else(|| FlowfullError::Storage("no storage configured".to_string()))?;
        let value = serde_json::to_string(user)?;
        storage.set_item(PUBFLOW_USER_DATA, &value).await
    }

    pub async fn get_user_data<T>(&self) -> Result<Option<T>>
    where
        T: DeserializeOwned,
    {
        let Some(storage) = &self.storage else {
            return Ok(None);
        };
        let Some(value) = storage.get_item(PUBFLOW_USER_DATA).await? else {
            return Ok(None);
        };
        Ok(Some(serde_json::from_str(&value)?))
    }

    pub async fn clear_user_data(&self) -> Result<()> {
        if let Some(storage) = &self.storage {
            storage.remove_item(PUBFLOW_USER_DATA).await?;
        }
        Ok(())
    }

    pub async fn clear_all(&self) -> Result<()> {
        self.clear_session().await?;
        self.clear_user_data().await
    }

    async fn cache_session_id(&self, session_id: String) {
        *self.cached_session_id.write().await = Some(session_id);
    }
}