use nodedb_types::sync::wire::{TokenRefreshAckMsg, TokenRefreshMsg};
use super::state::SyncClient;
pub const TOKEN_REFRESH_MIN_BACKOFF_MS: u64 = 5_000;
const TOKEN_REFRESH_MAX_BACKOFF_MS: u64 = 300_000;
const TOKEN_PROVIDER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
impl SyncClient {
pub async fn should_refresh_token(&self) -> bool {
if self.config.token_provider.is_none() || self.config.token_lifetime_secs == 0 {
return false;
}
if *self.token_refresh_pending.lock().await {
return false;
}
let set_at = *self.token_set_at_ms.lock().await;
let now = crate::runtime::now_millis();
let elapsed_ms = now.saturating_sub(set_at);
let threshold_ms = self.config.token_lifetime_secs * 800; elapsed_ms >= threshold_ms
}
pub async fn is_refresh_backoff_elapsed(&self) -> bool {
let last = *self.token_last_attempt_ms.lock().await;
if last == 0 {
return true;
}
let backoff = *self.token_refresh_backoff_ms.lock().await;
let now = crate::runtime::now_millis();
now.saturating_sub(last) >= backoff
}
pub async fn initiate_token_refresh(&self) -> Option<TokenRefreshMsg> {
let provider = self.config.token_provider.as_ref()?;
*self.token_last_attempt_ms.lock().await = crate::runtime::now_millis();
*self.token_refresh_pending.lock().await = true;
tracing::info!("initiating JWT token refresh");
let fut = provider();
let result = tokio::time::timeout(TOKEN_PROVIDER_TIMEOUT, fut).await;
match result {
Ok(Some(new_token)) => {
*self.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MIN_BACKOFF_MS;
Some(TokenRefreshMsg { new_token })
}
Ok(None) => {
tracing::warn!("token provider returned None; keeping push paused");
self.on_refresh_failure().await;
None
}
Err(_elapsed) => {
tracing::warn!(
timeout_secs = TOKEN_PROVIDER_TIMEOUT.as_secs(),
"token provider timed out; keeping push paused"
);
self.on_refresh_failure().await;
None
}
}
}
pub async fn handle_token_refresh_ack(&self, ack: &TokenRefreshAckMsg) {
*self.token_refresh_pending.lock().await = false;
if ack.success {
*self.token_set_at_ms.lock().await = crate::runtime::now_millis();
*self.push_paused_for_auth.lock().await = false;
*self.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MIN_BACKOFF_MS;
tracing::info!(
expires_in_secs = ack.expires_in_secs,
"JWT token refresh accepted by Origin"
);
} else {
tracing::warn!(
error = ack.error.as_deref().unwrap_or("unknown"),
"JWT token refresh rejected by Origin; keeping push paused"
);
self.apply_backoff().await;
}
}
pub async fn pause_for_auth(&self) {
*self.push_paused_for_auth.lock().await = true;
tracing::warn!("delta push paused — auth failure, awaiting token refresh");
}
pub async fn is_push_paused_for_auth(&self) -> bool {
*self.push_paused_for_auth.lock().await
}
async fn on_refresh_failure(&self) {
*self.token_refresh_pending.lock().await = false;
self.apply_backoff().await;
}
async fn apply_backoff(&self) {
let mut backoff = self.token_refresh_backoff_ms.lock().await;
*backoff = (*backoff * 2).min(TOKEN_REFRESH_MAX_BACKOFF_MS);
tracing::debug!(
next_backoff_ms = *backoff,
"token refresh backoff increased"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::client::SyncConfig;
use std::sync::Arc;
fn make_config_with_provider(returns: Option<&'static str>) -> SyncConfig {
let provider: crate::sync::client::config::TokenProvider =
Arc::new(move || Box::pin(async move { returns.map(|s| s.to_string()) }));
SyncConfig::new("wss://localhost:9090/sync", "test.jwt.token")
.with_token_provider(provider, 3600)
}
#[tokio::test]
async fn refresh_failure_keeps_push_paused() {
let config = make_config_with_provider(None); let client = SyncClient::new(config);
client.pause_for_auth().await;
let result = client.initiate_token_refresh().await;
assert!(result.is_none());
assert!(client.is_push_paused_for_auth().await);
assert!(!*client.token_refresh_pending.lock().await);
}
#[tokio::test]
async fn refresh_failure_applies_backoff() {
let config = make_config_with_provider(None);
let client = SyncClient::new(config);
client.pause_for_auth().await;
let initial_backoff = *client.token_refresh_backoff_ms.lock().await;
client.initiate_token_refresh().await;
let after_first = *client.token_refresh_backoff_ms.lock().await;
assert_eq!(
after_first,
(initial_backoff * 2).min(TOKEN_REFRESH_MAX_BACKOFF_MS)
);
client.initiate_token_refresh().await;
let after_second = *client.token_refresh_backoff_ms.lock().await;
assert_eq!(
after_second,
(after_first * 2).min(TOKEN_REFRESH_MAX_BACKOFF_MS)
);
}
#[tokio::test]
async fn refresh_success_clears_pause_and_resets_backoff() {
let config = make_config_with_provider(Some("fresh-jwt-token"));
let client = SyncClient::new(config);
client.pause_for_auth().await;
*client.token_refresh_backoff_ms.lock().await = 60_000;
let msg = client.initiate_token_refresh().await;
assert!(msg.is_some());
assert_eq!(msg.unwrap().new_token, "fresh-jwt-token");
client
.handle_token_refresh_ack(&TokenRefreshAckMsg {
success: true,
expires_in_secs: 3600,
error: None,
})
.await;
assert!(!client.is_push_paused_for_auth().await);
assert_eq!(
*client.token_refresh_backoff_ms.lock().await,
TOKEN_REFRESH_MIN_BACKOFF_MS
);
}
#[tokio::test]
async fn backoff_enforced_between_attempts() {
let config = make_config_with_provider(None);
let client = SyncClient::new(config);
*client.token_last_attempt_ms.lock().await = crate::runtime::now_millis();
*client.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MIN_BACKOFF_MS;
assert!(!client.is_refresh_backoff_elapsed().await);
*client.token_last_attempt_ms.lock().await = 0;
assert!(client.is_refresh_backoff_elapsed().await);
}
#[tokio::test]
async fn backoff_capped_at_max() {
let config = make_config_with_provider(None);
let client = SyncClient::new(config);
*client.token_refresh_backoff_ms.lock().await = TOKEN_REFRESH_MAX_BACKOFF_MS / 2 + 1;
client.apply_backoff().await;
assert_eq!(
*client.token_refresh_backoff_ms.lock().await,
TOKEN_REFRESH_MAX_BACKOFF_MS
);
client.apply_backoff().await;
assert_eq!(
*client.token_refresh_backoff_ms.lock().await,
TOKEN_REFRESH_MAX_BACKOFF_MS
);
}
}