rovenue/virtual_currencies/
reader.rs1use std::collections::BTreeMap;
2use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3use std::sync::Arc;
4
5use crate::cache::{CacheStore, VirtualCurrencyRepo};
6use crate::error::{RovenueError, RovenueResult};
7use crate::identity::IdentityManager;
8use crate::observer::{ChangeEvent, ObserverBus};
9use crate::time::Clock;
10use crate::transport::api::ApiEnvelope;
11use crate::transport::http_client::HttpClient;
12use crate::transport::types::HttpRequest;
13
14use super::types::VcBalancesWire;
15
16pub struct VirtualCurrencyReader {
17 store: Arc<CacheStore>,
18 identity: Arc<IdentityManager>,
19 http: Option<Arc<HttpClient>>,
20 bus: Option<Arc<ObserverBus>>,
21 clock: Option<Arc<dyn Clock>>,
22 last_refresh_ms: AtomicU64,
23 refreshing: AtomicBool,
24}
25
26impl VirtualCurrencyReader {
27 pub fn new(store: Arc<CacheStore>, identity: Arc<IdentityManager>) -> Self {
28 Self {
29 store,
30 identity,
31 http: None,
32 bus: None,
33 clock: None,
34 last_refresh_ms: AtomicU64::new(0),
35 refreshing: AtomicBool::new(false),
36 }
37 }
38 pub fn with_http(mut self, http: Arc<HttpClient>) -> Self {
39 self.http = Some(http);
40 self
41 }
42 pub fn with_observer_bus(mut self, bus: Arc<ObserverBus>) -> Self {
43 self.bus = Some(bus);
44 self
45 }
46 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
47 self.clock = Some(clock);
48 self
49 }
50
51 fn repo(&self) -> VirtualCurrencyRepo {
52 VirtualCurrencyRepo::new(Arc::clone(&self.store))
53 }
54
55 pub fn balances(&self) -> BTreeMap<String, i64> {
57 let scope = self.identity.current_user_scope();
58 self.repo().get_all(&scope).unwrap_or_default()
59 }
60
61 pub fn balance(&self, code: &str) -> i64 {
63 let scope = self.identity.current_user_scope();
64 self.repo().get(&scope, code).ok().flatten().unwrap_or(0)
65 }
66
67 pub fn refresh(&self) -> RovenueResult<()> {
69 let http = self.http.as_ref().ok_or(RovenueError::Internal())?;
70 let clock = self.clock.as_ref().ok_or(RovenueError::Internal())?;
71 let scope = self.identity.current_user_scope();
74 let wire_id = self.identity.rovenue_id();
75
76 let resp = http.get_json::<ApiEnvelope<VcBalancesWire>>(
77 HttpRequest::new("/v1/virtual-currencies/me").user_scope(&wire_id),
78 )?;
79 let body = resp.body.ok_or(RovenueError::Internal())?;
80 let balances: BTreeMap<String, i64> = body.data.balances.into_iter().collect();
81 self.set_balances(&scope, &balances, clock.now_unix_ms())
82 }
83
84 pub fn set_balances(
87 &self,
88 scope: &str,
89 balances: &BTreeMap<String, i64>,
90 now_ms: u64,
91 ) -> RovenueResult<()> {
92 let repo = self.repo();
93 let changed = repo
94 .get_all(scope)
95 .map(|prev| &prev != balances)
96 .unwrap_or(true);
97 repo.upsert_all(scope, balances, now_ms)?;
98 if changed {
99 if let Some(bus) = &self.bus {
100 bus.emit(ChangeEvent::VirtualCurrenciesChanged);
101 }
102 }
103 self.last_refresh_ms.store(now_ms, Ordering::Relaxed);
104 Ok(())
105 }
106
107 fn is_stale(&self, now: u64, staleness_ms: u64) -> bool {
108 now.saturating_sub(self.last_refresh_ms.load(Ordering::Relaxed)) > staleness_ms
109 }
110
111 pub fn maybe_refresh_async(self: &std::sync::Arc<Self>, staleness_ms: u64) {
112 let now = match &self.clock {
113 Some(c) => c.now_unix_ms(),
114 None => return,
115 };
116 if !self.is_stale(now, staleness_ms) {
117 return;
118 }
119 if self
120 .refreshing
121 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
122 .is_err()
123 {
124 return;
125 }
126 let this = std::sync::Arc::clone(self);
127 std::thread::spawn(move || {
128 struct ClearOnDrop<'a>(&'a std::sync::atomic::AtomicBool);
131 impl Drop for ClearOnDrop<'_> {
132 fn drop(&mut self) {
133 self.0.store(false, std::sync::atomic::Ordering::Release);
134 }
135 }
136 let _guard = ClearOnDrop(&this.refreshing);
137 let _ = this.refresh();
138 });
139 }
140}