1use std::time::Duration;
42
43use anyhow::Result;
44use bsv_tracker::{decode_envelope, Clock};
45use bsv_wallet_toolbox::{StorageSqlx, WalletServices};
46use serde::{Deserialize, Serialize};
47
48use crate::tracker_host::{self, TickOptions};
49
50#[derive(Debug, Clone)]
52pub struct FeedOptions {
53 pub min_pause: Duration,
55 pub max_pause: Duration,
57}
58
59impl Default for FeedOptions {
60 fn default() -> Self {
61 Self {
62 min_pause: Duration::from_secs(1),
63 max_pause: Duration::from_secs(300),
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct FeedFault {
72 pub kind: String,
76 pub cursor: String,
79 pub event_cursor: Option<String>,
81 pub detail: String,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct FeedStatus {
91 pub source: String,
93 pub state: String,
96 pub cursor: String,
98 pub delivered: u64,
100 pub reconnects: u64,
102 pub fault: Option<FeedFault>,
104 pub last_error: Option<String>,
106 pub updated_at: u64,
108}
109
110pub async fn read_status(storage: &StorageSqlx) -> Result<Option<FeedStatus>> {
112 let value: Option<String> =
113 sqlx::query_scalar("SELECT value FROM tracker_meta WHERE key = 'feed_status'")
114 .fetch_optional(storage.pool())
115 .await
116 .unwrap_or(None);
117 Ok(match value {
118 Some(v) => Some(serde_json::from_str(&v)?),
119 None => None,
120 })
121}
122
123async fn write_status(storage: &StorageSqlx, status: &FeedStatus) {
124 let written = sqlx::query(
125 "INSERT INTO tracker_meta (key, value) VALUES ('feed_status', ?) \
126 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
127 )
128 .bind(serde_json::to_string(status).unwrap_or_default())
129 .execute(storage.pool())
130 .await;
131 if let Err(e) = written {
132 tracing::warn!(error = %e, "chain feed: the status was not stored");
133 }
134}
135
136enum Leg {
138 Dropped { delivered: bool },
140 Stop(FeedFault),
142 Transient(String),
144}
145
146struct Feed<'a, V: ?Sized, C> {
147 base: String,
148 client: reqwest::Client,
149 storage: &'a StorageSqlx,
150 services: &'a V,
151 clock: &'a C,
152 opts: &'a TickOptions,
153 status: FeedStatus,
154}
155
156pub async fn run<V: WalletServices + ?Sized, C: Clock>(
159 base: &str,
160 storage: &StorageSqlx,
161 services: &V,
162 clock: &C,
163 opts: &TickOptions,
164 feed_opts: &FeedOptions,
165) -> FeedStatus {
166 let base = base.trim_end_matches('/').to_string();
167 let cursor = match tracker_host::feed_cursor(storage).await {
168 Ok(c) => c,
169 Err(e) => {
170 let status = FeedStatus {
171 source: base,
172 state: "stopped".into(),
173 cursor: "?".into(),
174 delivered: 0,
175 reconnects: 0,
176 fault: Some(FeedFault {
177 kind: "decode".into(),
178 cursor: "?".into(),
179 event_cursor: None,
180 detail: format!("the stored cursor: {e}"),
181 }),
182 last_error: None,
183 updated_at: clock.now(),
184 };
185 write_status(storage, &status).await;
186 return status;
187 }
188 };
189 let client = reqwest::Client::builder()
190 .connect_timeout(Duration::from_secs(10))
191 .build()
192 .unwrap_or_default();
193 let mut feed = Feed {
194 status: FeedStatus {
195 source: base.clone(),
196 state: "replaying".into(),
197 cursor: cursor.to_string(),
198 delivered: 0,
199 reconnects: 0,
200 fault: None,
201 last_error: None,
202 updated_at: clock.now(),
203 },
204 base,
205 client,
206 storage,
207 services,
208 clock,
209 opts,
210 };
211 let mut pause = feed_opts.min_pause;
212 let mut connects: u64 = 0;
213 loop {
214 feed.set_state("replaying").await;
215 let leg = match feed.replay().await {
216 Ok(()) => {
217 feed.set_state("following").await;
218 let leg = feed.stream(connects > 0).await;
219 connects += 1;
220 feed.status.reconnects = connects - 1;
221 leg
222 }
223 Err(leg) => leg,
224 };
225 match leg {
226 Leg::Stop(fault) => return feed.stop(fault).await,
227 Leg::Dropped { delivered } => {
228 if delivered {
229 pause = feed_opts.min_pause;
230 }
231 feed.status.last_error = Some("the stream ended without an error event".into());
232 tracing::info!(
233 cursor = %feed.status.cursor,
234 "chain feed: the stream dropped; reconnecting from the stored cursor"
235 );
236 }
237 Leg::Transient(e) => {
238 tracing::warn!(cursor = %feed.status.cursor, error = %e, "chain feed: retrying");
239 feed.status.last_error = Some(e);
240 }
241 }
242 feed.set_state("reconnecting").await;
243 tokio::time::sleep(pause).await;
244 pause = (pause * 2).min(feed_opts.max_pause);
245 }
246}
247
248impl<V: WalletServices + ?Sized, C: Clock> Feed<'_, V, C> {
249 async fn set_state(&mut self, state: &str) {
250 self.status.state = state.into();
251 self.status.updated_at = self.clock.now();
252 write_status(self.storage, &self.status).await;
253 }
254
255 async fn stop(mut self, fault: FeedFault) -> FeedStatus {
256 tracing::error!(
257 kind = %fault.kind,
258 cursor = %fault.cursor,
259 event_cursor = ?fault.event_cursor,
260 detail = %fault.detail,
261 "chain feed stopped at its cursor; nothing past it is delivered until the daemon restarts"
262 );
263 self.status.fault = Some(fault);
264 self.set_state("stopped").await;
265 self.status
266 }
267
268 fn fault(&self, kind: &str, event_cursor: Option<&str>, detail: impl Into<String>) -> Leg {
269 Leg::Stop(FeedFault {
270 kind: kind.into(),
271 cursor: self.status.cursor.clone(),
272 event_cursor: event_cursor.map(str::to_string),
273 detail: detail.into(),
274 })
275 }
276
277 async fn refusal(&self, resp: reqwest::Response) -> Leg {
280 let code = resp.status();
281 let body = resp.text().await.unwrap_or_default();
282 let detail = format!("HTTP {}: {}", code.as_u16(), body.trim());
283 match code.as_u16() {
284 409 => self.fault("cursorAhead", None, detail),
285 400 | 404 | 405 | 410 => self.fault("refused", None, detail),
286 _ => Leg::Transient(detail),
287 }
288 }
289
290 async fn deliver(&mut self, cursor: &str, bytes: &[u8]) -> std::result::Result<(), Leg> {
293 let Ok(at) = cursor.trim().parse::<u64>() else {
294 return Err(self.fault("decode", Some(cursor), "a cursor that is not a decimal"));
295 };
296 let event = match decode_envelope(bytes) {
297 Ok(event) => event,
298 Err(fault) => return Err(self.fault("decode", Some(cursor), fault.to_string())),
299 };
300 let delivery = tracker_host::deliver_chain_event(
301 self.storage,
302 self.services,
303 self.clock,
304 self.opts,
305 at,
306 event,
307 )
308 .await
309 .map_err(|e| Leg::Transient(format!("the delivery at {cursor} was not stored: {e}")))?;
310 if at > self.status.cursor.parse().unwrap_or(0) {
311 self.status.cursor = at.to_string();
312 self.status.delivered += 1;
313 }
314 self.status.updated_at = self.clock.now();
318 write_status(self.storage, &self.status).await;
319 for moved in &delivery.moved {
320 tracing::info!(
321 cursor = at,
322 kind = delivery.kind,
323 txid = %moved.txid,
324 word = %moved.word,
325 reask = ?moved.reask,
326 suspect = moved.suspect,
327 "chain feed: a chain event moved a tracked transaction"
328 );
329 }
330 Ok(())
331 }
332
333 async fn replay(&mut self) -> std::result::Result<(), Leg> {
335 loop {
336 let since = self.status.cursor.clone();
337 let url = format!("{}/events?since={since}", self.base);
338 let resp = self
339 .client
340 .get(&url)
341 .send()
342 .await
343 .map_err(|e| Leg::Transient(format!("GET /events: {e}")))?;
344 if !resp.status().is_success() {
345 return Err(self.refusal(resp).await);
346 }
347 let page: Page = match resp.json().await {
348 Ok(page) => page,
349 Err(e) => return Err(self.fault("decode", None, format!("a page: {e}"))),
350 };
351 for item in &page.events {
352 let bytes = serde_json::to_vec(&item.event).unwrap_or_default();
353 self.deliver(&item.cursor, &bytes).await?;
354 }
355 if !page.has_more {
356 return Ok(());
357 }
358 if page.events.is_empty() || page.cursor == since {
359 return Err(Leg::Transient(format!(
360 "a page at {since} said hasMore and moved no cursor"
361 )));
362 }
363 }
364 }
365
366 async fn stream(&mut self, reconnect: bool) -> Leg {
368 let since = self.status.cursor.clone();
369 let url = format!("{}/events/stream?since={since}", self.base);
370 let mut request = self.client.get(&url).header("Accept", "text/event-stream");
371 if reconnect {
372 request = request.header("Last-Event-ID", &since);
373 }
374 let mut resp = match request.send().await {
375 Ok(resp) => resp,
376 Err(e) => return Leg::Transient(format!("GET /events/stream: {e}")),
377 };
378 if !resp.status().is_success() {
379 return self.refusal(resp).await;
380 }
381 let mut buffer: Vec<u8> = Vec::new();
382 let mut delivered = false;
383 loop {
384 let chunk = match resp.chunk().await {
385 Ok(Some(chunk)) => chunk,
386 Ok(None) | Err(_) => return Leg::Dropped { delivered },
387 };
388 buffer.extend(chunk.iter().filter(|b| **b != b'\r'));
389 while let Some(end) = buffer.windows(2).position(|w| w == b"\n\n") {
390 let block: Vec<u8> = buffer.drain(..end + 2).collect();
391 let message = SseMessage::parse(&String::from_utf8_lossy(&block));
392 if message.event.as_deref() == Some("error") {
393 return self.fault(
394 "feedUnavailable",
395 message.id.as_deref(),
396 message.data.unwrap_or_default(),
397 );
398 }
399 let Some(data) = message.data else {
400 continue;
401 };
402 let Some(id) = message.id else {
403 return self.fault("decode", None, format!("an envelope with no id: {data}"));
404 };
405 if let Err(leg) = self.deliver(&id, data.as_bytes()).await {
406 return leg;
407 }
408 delivered = true;
409 }
410 }
411 }
412}
413
414#[derive(Debug, Deserialize)]
416#[serde(rename_all = "camelCase")]
417struct Page {
418 cursor: String,
419 events: Vec<PageItem>,
420 has_more: bool,
421}
422
423#[derive(Debug, Deserialize)]
424struct PageItem {
425 cursor: String,
426 event: serde_json::Value,
427}
428
429#[derive(Debug, Default, PartialEq, Eq)]
432struct SseMessage {
433 id: Option<String>,
434 event: Option<String>,
435 data: Option<String>,
436}
437
438impl SseMessage {
439 fn parse(block: &str) -> Self {
440 let mut message = Self::default();
441 for line in block.lines() {
442 if line.is_empty() || line.starts_with(':') {
443 continue;
444 }
445 let (field, value) = line.split_once(':').unwrap_or((line, ""));
446 let value = value.strip_prefix(' ').unwrap_or(value);
447 match field {
448 "id" => message.id = Some(value.to_string()),
449 "event" => message.event = Some(value.to_string()),
450 "data" => {
451 message.data = Some(match message.data.take() {
452 Some(previous) => format!("{previous}\n{value}"),
453 None => value.to_string(),
454 })
455 }
456 _ => {}
457 }
458 }
459 message
460 }
461}
462
463#[cfg(test)]
464#[path = "chain_feed_tests.rs"]
465mod tests;