Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/instance/backend/
remote.rs

1//! [`RemoteBackend`]: the seam backed by a service connection.
2
3use async_trait::async_trait;
4
5use super::Backend;
6use crate::{
7    Result,
8    auth::SigKey,
9    backend::{InstanceMetadata, VerificationStatus},
10    entry::{Entry, ID},
11    instance::WriteSource,
12    service::{client::RemoteConnection, protocol::ReadScope},
13    snapshot::Snapshot,
14};
15
16/// A [`Backend`] that translates every storage operation to a wire RPC over a
17/// shared [`RemoteConnection`].
18///
19/// The only per-handle state is the acting identity: `None` means "use the
20/// connection's current session identity" (the instance-level backend), and
21/// `Some(k)` means "act as `k`" (a `Database` handle opened with key `k`).
22/// Every clone shares the same socket and session — additional keys are
23/// proof-of-possession registered into the connection's keyset by the handle
24/// constructors, not by holding a separate connection.
25///
26/// Tree-scoped methods use the `tree` argument the caller already supplies
27/// (`Transaction` passes the owning database's root), so no root is bound here.
28/// `get` derives its gating tree server-side from the fetched entry, so it
29/// passes `ID::default()` as the (waved-through) request root.
30///
31/// CRDT-state caching is two-tiered: a connection-scoped process-lifetime LRU
32/// (tier 1) backed by the daemon's unified scope-keyed cache (tier 2) reached
33/// via `GetCachedCrdtState` / `CacheCrdtState` RPCs.
34#[derive(Debug, Clone)]
35pub struct RemoteBackend {
36    conn: RemoteConnection,
37    identity: Option<SigKey>,
38}
39
40impl RemoteBackend {
41    pub fn new(conn: RemoteConnection, identity: Option<SigKey>) -> Self {
42        Self { conn, identity }
43    }
44
45    /// The acting identity for authenticated RPCs: the bound per-handle
46    /// identity, else the connection's current session identity.
47    fn identity(&self) -> SigKey {
48        self.identity
49            .clone()
50            .or_else(|| self.conn.session_identity())
51            .unwrap_or_default()
52    }
53}
54
55#[async_trait]
56impl Backend for RemoteBackend {
57    async fn get(&self, id: &ID) -> Result<Entry> {
58        // `ID::default()` is never a real database, so the pre-dispatch gate
59        // waves it through; the server then gates post-fetch against the
60        // fetched entry's owning tree using our identity.
61        self.conn
62            .db_get_entry(ID::default(), self.identity(), id.clone())
63            .await
64    }
65
66    async fn snapshot(&self, tree: &ID) -> Result<Snapshot> {
67        match self
68            .conn
69            .get_verified_tips(tree.clone(), self.identity())
70            .await
71        {
72            Ok(snapshot) => Ok(snapshot),
73            Err(e) if e.is_not_found() => Ok(Snapshot::EMPTY),
74            Err(e) => Err(e),
75        }
76    }
77
78    async fn store_snapshot(&self, tree: &ID, store: &str) -> Result<Snapshot> {
79        let tree_tips = match self
80            .conn
81            .get_verified_tips(tree.clone(), self.identity())
82            .await
83        {
84            Ok(tips) => tips,
85            Err(e) if e.is_not_found() => return Ok(Snapshot::EMPTY),
86            Err(e) => return Err(e),
87        };
88        if tree_tips.is_empty() {
89            return Ok(Snapshot::EMPTY);
90        }
91        match self
92            .conn
93            .store_snapshot_at(
94                tree.clone(),
95                self.identity(),
96                store.to_string(),
97                tree_tips.into_tips(),
98            )
99            .await
100        {
101            Ok(snapshot) => Ok(snapshot),
102            Err(e) if e.is_not_found() => Ok(Snapshot::EMPTY),
103            Err(e) => Err(e),
104        }
105    }
106
107    async fn store_snapshot_at(
108        &self,
109        tree: &ID,
110        store: &str,
111        main_snapshot: &Snapshot,
112    ) -> Result<Snapshot> {
113        match self
114            .conn
115            .store_snapshot_at(
116                tree.clone(),
117                self.identity(),
118                store.to_string(),
119                main_snapshot.tips().to_vec(),
120            )
121            .await
122        {
123            Ok(snapshot) => Ok(snapshot),
124            Err(e) if e.is_not_found() => Ok(Snapshot::EMPTY),
125            Err(e) => Err(e),
126        }
127    }
128
129    async fn store_at(&self, tree: &ID, store: &str, snapshot: &Snapshot) -> Result<Vec<Entry>> {
130        self.conn
131            .get_store_entries(
132                tree.clone(),
133                self.identity(),
134                store.to_string(),
135                snapshot.tips().to_vec(),
136                ReadScope::Verified,
137            )
138            .await
139    }
140
141    async fn find_merge_base(&self, tree: &ID, store: &str, entry_ids: &[ID]) -> Result<ID> {
142        let state = self
143            .conn
144            .compute_merge_state(
145                tree.clone(),
146                self.identity(),
147                store.to_string(),
148                entry_ids.to_vec(),
149            )
150            .await?;
151        Ok(state.merge_base)
152    }
153
154    async fn get_path_from_to(
155        &self,
156        tree: &ID,
157        store: &str,
158        _from_id: &ID,
159        to_ids: &[ID],
160    ) -> Result<Vec<ID>> {
161        // The server fuses LCA + path against `to_ids` in one round-trip, so a
162        // separately-supplied `from_id` LCA isn't replayed.
163        let state = self
164            .conn
165            .compute_merge_state(
166                tree.clone(),
167                self.identity(),
168                store.to_string(),
169                to_ids.to_vec(),
170            )
171            .await?;
172        Ok(state.path)
173    }
174
175    async fn get_cached_crdt_state(
176        &self,
177        tree: &ID,
178        entry_id: &ID,
179        store: &str,
180    ) -> Result<Option<Vec<u8>>> {
181        // Tier 1: connection-shared process-lifetime LRU.
182        if let Some(blob) = self.conn.cache_get(tree, entry_id, store) {
183            return Ok(Some(blob));
184        }
185        // Tier 2: daemon-side unified cache, durable across sessions.
186        let blob = self
187            .conn
188            .get_cached_crdt_state_remote(
189                tree.clone(),
190                self.identity(),
191                store.to_string(),
192                entry_id.clone(),
193            )
194            .await?;
195        if let Some(b) = &blob {
196            self.conn
197                .cache_put(tree.clone(), entry_id.clone(), store.to_string(), b.clone());
198        }
199        Ok(blob)
200    }
201
202    async fn cache_crdt_state(
203        &self,
204        tree: &ID,
205        entry_id: &ID,
206        store: &str,
207        state: Vec<u8>,
208    ) -> Result<()> {
209        // Tier 1: stash locally first so a same-session re-read hits even if
210        // the tier-2 write later fails.
211        self.conn.cache_put(
212            tree.clone(),
213            entry_id.clone(),
214            store.to_string(),
215            state.clone(),
216        );
217        // Tier 2: propagate to the daemon. Awaited so wire errors surface.
218        self.conn
219            .cache_crdt_state_remote(
220                tree.clone(),
221                self.identity(),
222                store.to_string(),
223                entry_id.clone(),
224                state,
225            )
226            .await
227    }
228
229    async fn put(&self, entry: Entry) -> Result<()> {
230        let tree_root = entry.root().unwrap_or_else(|| entry.id());
231        self.conn
232            .submit_signed_entry(tree_root, self.identity(), entry)
233            .await
234    }
235
236    async fn write_entry(
237        &self,
238        _verification: VerificationStatus,
239        entry: Entry,
240        _source: WriteSource,
241    ) -> Result<()> {
242        // The server stores the submitted entry `Unverified` and runs its own
243        // verification pass; a client-asserted status is never trusted.
244        let tree_root = entry.root().unwrap_or_else(|| entry.id());
245        self.conn
246            .submit_signed_entry(tree_root, self.identity(), entry)
247            .await
248    }
249
250    async fn get_instance_metadata(&self) -> Result<Option<InstanceMetadata>> {
251        self.conn.get_instance_metadata().await
252    }
253
254    async fn set_instance_metadata(&self, metadata: &InstanceMetadata) -> Result<()> {
255        self.conn.set_instance_metadata(metadata).await
256    }
257
258    fn remote_connection(&self) -> Option<RemoteConnection> {
259        Some(self.conn.clone())
260    }
261}