braid_http/client/
utils.rs1use 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 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}