1use std::collections::BTreeMap;
3
4use ssp2::model::{Frame, MediaType, Message, MsgKind, SubStatus};
5use ssp2::{decode_message, encode_message};
6
7use crate::client::RequestMeta;
8use crate::{
9 BlobUploadGrant, ProgressObserver, ProgressPhase, SegmentRequest, SyncOutcome, Transport,
10 TransportError,
11};
12
13pub(crate) fn now_ms(fixed: Option<i64>) -> i64 {
14 fixed.unwrap_or_else(|| {
15 std::time::SystemTime::now()
16 .duration_since(std::time::UNIX_EPOCH)
17 .expect("system clock predates Unix epoch")
18 .as_millis() as i64
19 })
20}
21
22pub(crate) struct PendingUpload {
23 pub id: String,
24 pub bytes: Result<Vec<u8>, TransportError>,
25 pub media_type: Option<String>,
26}
27
28pub(crate) fn upload_one(
29 transport: &mut dyn Transport,
30 id: &str,
31 bytes: &[u8],
32 media_type: Option<&str>,
33 fixed_now: Option<i64>,
34) -> Result<(), TransportError> {
35 match transport.blob_upload_grant(id, bytes.len() as u64, media_type)? {
36 BlobUploadGrant::Present => return Ok(()),
37 BlobUploadGrant::Url {
38 url,
39 url_expires_at_ms,
40 } => {
41 if url_expires_at_ms.is_none_or(|expiry| expiry > now_ms(fixed_now))
42 && transport.blob_put_url(&url, bytes, media_type).is_ok()
43 {
44 return Ok(());
45 }
46 }
47 BlobUploadGrant::None => {}
48 }
49 transport.blob_upload(id, bytes, media_type)
50}
51
52pub struct PreparedSyncRound {
55 pub(crate) id: uuid::Uuid,
56 pub(crate) started_at_ms: i64,
57 pub(crate) meta: RequestMeta,
58 pub(crate) message: Message,
59 pub(crate) realtime: bool,
60 pub(crate) uploads: std::collections::VecDeque<PendingUpload>,
61 pub(crate) scopes: BTreeMap<String, String>,
62 #[cfg(feature = "bench-internals")]
63 pub(crate) benchmark_phases: crate::bench::Recorder,
64 pub(crate) progress: ProgressObserver,
65 pub(crate) fixed_now: Option<i64>,
66}
67
68pub struct CompletedSyncRound {
70 pub(crate) prepared: PreparedSyncRound,
71 pub(crate) exchange: ExchangeResult,
72}
73
74pub(crate) enum ExchangeResult {
75 Upload {
76 id: String,
77 result: Result<(), TransportError>,
78 },
79 Reply {
80 response: Result<Message, TransportError>,
81 transport_failed: bool,
82 downloads: DownloadResults,
83 },
84}
85
86pub enum AppliedSyncRound {
89 Continue(PreparedSyncRound),
90 Complete {
91 outcome: SyncOutcome,
92 controls: Vec<String>,
93 more: bool,
95 bootstrap_advanced: bool,
97 },
98}
99
100impl PreparedSyncRound {
101 pub fn exchange(mut self, transport: &mut dyn Transport) -> CompletedSyncRound {
104 if let Some(upload) = self.uploads.pop_front() {
105 let result = upload.bytes.and_then(|bytes| {
106 upload_one(
107 transport,
108 &upload.id,
109 &bytes,
110 upload.media_type.as_deref(),
111 self.fixed_now,
112 )
113 });
114 return CompletedSyncRound {
115 prepared: self,
116 exchange: ExchangeResult::Upload {
117 id: upload.id,
118 result,
119 },
120 };
121 }
122 let mut transport_failed = false;
123 let response = (|| {
124 #[cfg(feature = "bench-internals")]
125 let encode_phase = self
126 .benchmark_phases
127 .start(crate::bench::Phase::RequestEncode);
128 let request = encode_message(&self.message);
129 #[cfg(feature = "bench-internals")]
130 drop(encode_phase);
131 let round = if self.realtime {
132 transport.realtime_sync(&request)
133 } else {
134 transport.sync(&request)
135 };
136 transport_failed = round.is_err();
137 let bytes = round?;
138 #[cfg(feature = "bench-internals")]
139 let _decode_phase = self
140 .benchmark_phases
141 .start(crate::bench::Phase::ResponseDecode);
142 let response = decode_message(&bytes)
143 .map_err(|e| TransportError::new(e.code.as_str(), e.detail))?;
144 if response.msg_kind != MsgKind::Response {
145 return Err(TransportError::new(
146 "sync.invalid_request",
147 "expected a response message",
148 ));
149 }
150 Ok(response)
151 })();
152 let mut downloads = DownloadResults::default();
153 if let Ok(response) = &response {
154 if matches!(response.frames.first(), Some(Frame::RespHeader { required_schema_version: None, reset_required: Some(false), log_epoch: Some(epoch), .. }) if self.message.frames.first().is_some_and(|f| matches!(f, Frame::ReqHeader { log_epoch: Some(request_epoch), .. } if request_epoch == epoch)))
156 {
157 let mut scope: Option<(&str, &str)> = None;
158 for frame in &response.frames {
159 match frame {
160 Frame::SubStart {
161 id,
162 status: SubStatus::Active,
163 ..
164 } => {
165 scope = self
166 .scopes
167 .get(id)
168 .map(|scopes| (id.as_str(), scopes.as_str()));
169 }
170 Frame::SubStart { .. } | Frame::SubEnd { .. } => scope = None,
171 Frame::Error { .. } => break,
172 Frame::SegmentRef {
173 segment_id,
174 table,
175 media_type,
176 url,
177 url_expires_at_ms,
178 byte_length,
179 row_count,
180 ..
181 } => {
182 let Some((id, scopes)) = scope else { continue };
183 let advertised = match media_type {
184 MediaType::Rows => self.meta.accept & 3 != 0,
185 MediaType::Sqlite => self.meta.accept & 4 != 0,
186 };
187 if !advertised || (url.is_some() && self.meta.accept & 8 == 0) {
188 break;
189 }
190 let started_at = now_ms(self.fixed_now);
191 let key = match url {
192 Some(url) => DownloadKey::Url(url.clone()),
193 None => DownloadKey::Segment(
194 segment_id.clone(),
195 table.clone(),
196 scopes.to_owned(),
197 ),
198 };
199 self.progress.update(|p| {
200 p.phase = ProgressPhase::Download;
201 p.subscription_id = Some(id.to_owned());
202 p.table = Some(table.clone());
203 p.segment_id = Some(segment_id.clone());
204 p.bytes_received = 0;
205 p.bytes_total = u64::try_from(*byte_length).ok();
206 p.rows_processed = 0;
207 p.rows_total = u64::try_from(*row_count).ok();
208 });
209 let mut on_progress =
210 |bytes| self.progress.update(|p| p.bytes_received = bytes);
211 let bytes = if let Some(url) = url {
212 if url_expires_at_ms.is_some_and(|expiry| expiry <= started_at) {
213 Err(TransportError::new(
214 "sync.segment_expired",
215 "signed segment URL expired before fetch",
216 ))
217 } else {
218 transport.fetch_url(url, &mut on_progress)
219 }
220 } else {
221 transport.download_segment(
222 &SegmentRequest {
223 segment_id: segment_id.clone(),
224 table: table.clone(),
225 requested_scopes_json: scopes.to_owned(),
226 },
227 &mut on_progress,
228 )
229 };
230 let failed = bytes.is_err();
231 downloads.entries.push_back((key, started_at, bytes));
232 if failed {
233 break;
234 }
235 }
236 _ => {}
237 }
238 }
239 }
240 }
241 CompletedSyncRound {
242 prepared: self,
243 exchange: ExchangeResult::Reply {
244 response,
245 downloads,
246 transport_failed,
247 },
248 }
249 }
250}
251
252#[derive(PartialEq)]
253enum DownloadKey {
254 Url(String),
255 Segment(String, String, String),
256}
257
258#[derive(Default)]
259pub(crate) struct DownloadResults {
260 entries: std::collections::VecDeque<(DownloadKey, i64, Result<Vec<u8>, TransportError>)>,
261 pub controls: Vec<String>,
262}
263
264impl DownloadResults {
265 fn take(&mut self, key: DownloadKey) -> Result<Vec<u8>, TransportError> {
266 let position = self
267 .entries
268 .iter()
269 .position(|(candidate, ..)| *candidate == key);
270 position
271 .and_then(|i| self.entries.remove(i))
272 .map(|(_, _, bytes)| bytes)
273 .unwrap_or_else(|| {
274 Err(TransportError::new(
275 "client.round_download_missing",
276 "round apply requested an uncaptured segment",
277 ))
278 })
279 }
280}
281
282impl Transport for DownloadResults {
283 fn sync(&mut self, _: &[u8]) -> Result<Vec<u8>, TransportError> {
284 Err(TransportError::new(
285 "client.round_network_on_owner",
286 "network requests must execute outside the client owner",
287 ))
288 }
289 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
290 self.sync(request)
291 }
292 fn download_segment(
293 &mut self,
294 request: &SegmentRequest,
295 _: &mut dyn FnMut(u64),
296 ) -> Result<Vec<u8>, TransportError> {
297 self.take(DownloadKey::Segment(
298 request.segment_id.clone(),
299 request.table.clone(),
300 request.requested_scopes_json.clone(),
301 ))
302 }
303 fn fetch_url(&mut self, url: &str, _: &mut dyn FnMut(u64)) -> Result<Vec<u8>, TransportError> {
304 self.take(DownloadKey::Url(url.to_owned()))
305 }
306 fn segment_fetch_started_at(&self, url: &str) -> Option<i64> {
307 self.entries
308 .iter()
309 .find_map(|(key, time, _)| (*key == DownloadKey::Url(url.to_owned())).then_some(*time))
310 }
311 fn realtime_connect(&mut self) -> Result<(), TransportError> {
312 Err(TransportError::new(
313 "client.round_network_on_owner",
314 "network connections must execute outside the client owner",
315 ))
316 }
317 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
318 self.controls.push(text.to_owned());
319 Ok(())
320 }
321 fn realtime_close(&mut self) -> Result<(), TransportError> {
322 Ok(())
323 }
324}