Skip to main content

braid_http/client/
utils.rs

1//! Utility functions for the Braid HTTP client.
2
3use crate::client::parser::Message;
4use crate::error::{BraidError, Result};
5use crate::protocol;
6use crate::types::{Update, Version};
7use bytes::{Bytes, BytesMut};
8use std::time::Duration;
9
10pub fn parse_content_range(header: &str) -> Result<(String, String)> {
11    protocol::parse_content_range(header)
12}
13
14pub fn format_content_range(unit: &str, range: &str) -> String {
15    protocol::format_content_range(unit, range)
16}
17
18pub fn parse_heartbeat(value: &str) -> Result<Duration> {
19    let trimmed = value.trim();
20    let num_str = if let Some(s) = trimmed.strip_suffix("ms") {
21        s
22    } else if let Some(s) = trimmed.strip_suffix('s') {
23        s
24    } else {
25        trimmed
26    };
27
28    let num: f64 = num_str
29        .parse()
30        .map_err(|_| BraidError::HeaderParse(format!("Invalid heartbeat: {}", value)))?;
31    Ok(Duration::from_secs_f64(num))
32}
33
34pub fn version_to_json_string(version: &str) -> String {
35    format!("\"{}\"", version)
36}
37
38pub fn is_retryable_status(status: u16) -> bool {
39    matches!(status, 408 | 425 | 429 | 502 | 503 | 504)
40}
41
42pub fn is_access_denied_status(status: u16) -> bool {
43    matches!(status, 401 | 403)
44}
45
46pub fn exponential_backoff(attempt: u32, base_ms: u64) -> Duration {
47    let delay_ms = base_ms * 2_u64.pow(attempt.min(10));
48    Duration::from_millis(delay_ms)
49}
50
51pub fn merge_bodies(body1: &Bytes, body2: &Bytes) -> Bytes {
52    let mut result = BytesMut::with_capacity(body1.len() + body2.len());
53    result.extend_from_slice(body1);
54    result.extend_from_slice(body2);
55    result.freeze()
56}
57
58pub fn message_to_update(msg: Message) -> Update {
59    let version = extract_version(&msg.headers).unwrap_or_else(|| {
60        // Fallback: If version is missing, we use a unique temporary version for this update
61        // to avoid colliding with other "missing" states, but ideally we should skip.
62        // For braid.org/diamond-types, we should use a valid-looking id if we MUST have one.
63        let temp_id = "temp-0".to_string();
64        tracing::warn!(
65            "[BraidHTTP] Version header missing in message from {}. Using temporary ID: {}",
66            msg.url.as_deref().unwrap_or("unknown"),
67            temp_id
68        );
69        Version::new(&temp_id)
70    });
71
72    let mut builder = if !msg.patches.is_empty() {
73        Update::patched(version, msg.patches)
74    } else {
75        let body = String::from_utf8_lossy(&msg.body).to_string();
76        Update::snapshot(version, body)
77    };
78
79    if let Some(parents) = extract_parents(&msg.headers) {
80        for parent in parents {
81            builder = builder.with_parent(parent);
82        }
83    }
84
85    if let Some(merge_type) = msg.headers.get("merge-type") {
86        builder = builder.with_merge_type(merge_type.clone());
87    }
88
89    builder.url = msg.url;
90    builder
91}
92
93fn extract_version(headers: &std::collections::BTreeMap<String, String>) -> Option<Version> {
94    let version = headers
95        .get("current-version")
96        .or_else(|| headers.get("version"))
97        .or_else(|| headers.get("Version"))
98        .or_else(|| headers.get("Current-Version"))
99        .and_then(|v| {
100            let trimmed = v.trim();
101            if trimmed.is_empty() || trimmed == "\"\"" {
102                return None;
103            }
104            protocol::parse_version_header(v).ok()
105        })
106        .and_then(|mut v| v.pop());
107
108    if version.is_none() {
109        tracing::info!(
110            "[BraidHTTP] extract_version failed. Headers were: {:?}",
111            headers
112        );
113    } else {
114        tracing::debug!("[BraidHTTP] Parsed version: {:?}", version);
115    }
116    version
117}
118
119fn extract_parents(headers: &std::collections::BTreeMap<String, String>) -> Option<Vec<Version>> {
120    let parents = headers
121        .get("parents")
122        .and_then(|v| protocol::parse_version_header(v).ok());
123
124    if parents.is_none() {
125        tracing::debug!(
126            "[BraidHTTP] Parents header missing or failed to parse. Headers: {:?}",
127            headers
128        );
129    } else {
130        tracing::debug!("[BraidHTTP] Parsed parents: {:?}", parents);
131    }
132    parents
133}
134
135#[cfg(not(target_arch = "wasm32"))]
136pub fn spawn_task<F>(future: F)
137where
138    F: std::future::Future<Output = ()> + Send + 'static,
139{
140    tokio::spawn(future);
141}
142
143#[cfg(target_arch = "wasm32")]
144pub fn spawn_task<F>(future: F)
145where
146    F: std::future::Future<Output = ()> + 'static,
147{
148    wasm_bindgen_futures::spawn_local(future);
149}
150
151#[cfg(not(target_arch = "wasm32"))]
152pub async fn sleep(duration: Duration) {
153    tokio::time::sleep(duration).await;
154}
155
156#[cfg(target_arch = "wasm32")]
157pub async fn sleep(duration: Duration) {
158    gloo_timers::future::sleep(duration).await;
159}