rovenue/cache/
exposure.rs

1use std::sync::Arc;
2
3use rusqlite::params;
4
5use super::store::CacheStore;
6use crate::error::RovenueResult;
7
8/// Append-only dedup ledger: one row per (scope, experiment, variant) that
9/// has already been reported as exposed. A variant change yields a new
10/// (experiment_id, variant_id) pair → a fresh exposure.
11pub struct ExposureRepo {
12    store: Arc<CacheStore>,
13}
14
15impl ExposureRepo {
16    pub fn new(store: Arc<CacheStore>) -> Self {
17        Self { store }
18    }
19
20    pub fn is_exposed(
21        &self,
22        user_scope: &str,
23        experiment_id: &str,
24        variant_id: &str,
25    ) -> RovenueResult<bool> {
26        self.store.with_conn(|c| {
27            let mut stmt = c.prepare(
28                "SELECT 1 FROM experiment_exposure
29                 WHERE user_scope = ?1 AND experiment_id = ?2 AND variant_id = ?3",
30            )?;
31            let mut rows = stmt.query(params![user_scope, experiment_id, variant_id])?;
32            Ok(rows.next()?.is_some())
33        })
34    }
35
36    pub fn mark(
37        &self,
38        user_scope: &str,
39        experiment_id: &str,
40        variant_id: &str,
41        exposed_at_ms: u64,
42    ) -> RovenueResult<()> {
43        self.store.with_conn(|c| {
44            c.execute(
45                "INSERT OR IGNORE INTO experiment_exposure
46                   (user_scope, experiment_id, variant_id, exposed_at_ms)
47                 VALUES (?1, ?2, ?3, ?4)",
48                params![user_scope, experiment_id, variant_id, exposed_at_ms as i64],
49            )?;
50            Ok(())
51        })
52    }
53}