coven/read_handle.rs
1//! The read-only handle: a same-store secondary reader.
2//!
3//! Where [`CovenHandle`](crate::CovenHandle) is the one full handle a host opens to
4//! drive rows, blobs, and sync, [`CovenReadHandle`] is the deliberately narrow
5//! counterpart for a second reader of the *same* store — a separate process (the
6//! macOS File Provider extension) or a second in-process handle — that must read
7//! while another handle holds the writer open.
8//!
9//! It is opened with [`Coven::builder(store_dir, cfg).open_read_only()`](crate::CovenBuilder::open_read_only)
10//! and exposes reads only: SQL queries against the connection coven owns, and blob
11//! reads (local store, pinned/evictable cache, or a cloud fetch into the cache).
12//! There is no write, sync-connection, migration, or stamp API on it — those are absent
13//! by construction, so a reader cannot mutate the synced state a concurrent writer
14//! owns. [`read`](CovenReadHandle::read) supplies query operations without
15//! exposing the retained connection.
16//!
17//! It takes no store lock: it coexists with the writer that holds the exclusive
18//! `.coven-lock`, and with any number of other read-only opens (a read-only
19//! connection cannot write, so the single-writer lock does not apply to it).
20//! SQLite's locking coordinates these read-only connections with the writer.
21
22use std::sync::Arc;
23
24use crate::coven::CovenResult;
25use crate::store_blobs::{StoreBlobAccess, StoreBlobs};
26use crate::store_cloud_storage::StoreCloudStorage;
27use crate::store_security::StoreSecurity;
28use crate::store_sync::ConfigProvider;
29use coven_database::store::StoreReads;
30use coven_database::Database;
31use coven_database::StoreDatabase;
32use coven_foundation::clock::ClockRef;
33use coven_foundation::store_dir::StoreDir;
34use coven_keys::encryption::SealError;
35use coven_keys::keys::{DeviceIdentityCustody, MasterKeyCustody, StoreKeys};
36use coven_protocol::blob::RowBlobRef;
37use coven_replication::sync::store::blob::{LocalStoreBlobAccess, StoreBlobCache};
38use coven_replication::sync::{BlobCacheError, BlobStream};
39
40/// A read-only handle over one coven store, for a same-store secondary reader.
41///
42/// Open it with
43/// [`Coven::builder(store_dir, cfg).open_read_only()`](crate::CovenBuilder::open_read_only).
44/// Cheap to [`clone`](Clone) — every field is shared (an `Arc` or a `Clone` handle),
45/// so a clone reads the same database and storage as the original.
46///
47/// # What it can do
48///
49/// - **Rows** — read via [`read`](Self::read). The closure receives a
50/// [`SqlReadContext`](crate::SqlReadContext) that exposes queries without the
51/// retained connection.
52/// - **Blobs** — [`read_blob`](Self::read_blob) and
53/// [`open_blob_stream`](Self::open_blob_stream) resolve a blob's locality and serve
54/// it from the local store, the cache, or a cloud fetch into the per-device cache.
55/// [`is_pinned`](Self::is_pinned) reports whether a set is kept offline.
56///
57/// It builds read storage from the current [`crate::Config`] on a cloud miss, exactly as a
58/// home-less full handle does — there is no sync loop to reuse.
59#[derive(Clone)]
60pub struct CovenReadHandle {
61 reads: StoreReads,
62 blobs: StoreBlobs,
63 security: StoreSecurity,
64}
65
66impl CovenReadHandle {
67 #[allow(clippy::too_many_arguments)]
68 pub(crate) fn new(
69 db: Database,
70 reads: StoreReads,
71 store_dir: StoreDir,
72 config_provider: ConfigProvider,
73 key_service: StoreKeys,
74 key_custody: Arc<dyn MasterKeyCustody>,
75 identity_custody: Arc<dyn DeviceIdentityCustody>,
76 oauth_clients: coven_storage::oauth::OAuthClients,
77 clock: ClockRef,
78 cloudkit_ops: Option<Arc<dyn coven_storage::cloud::cloudkit::CloudKitOps>>,
79 blob_chunking: coven_storage::BlobChunking,
80 ) -> Self {
81 let database = StoreDatabase::from_database(db);
82 let cloud_homes = coven_storage::cloud::CloudHomeFactory::new(oauth_clients);
83 let credentials = coven_keys::keys::CloudHomeCredentialsOwner::new(key_service.clone());
84 let security = StoreSecurity::new(
85 key_service,
86 key_custody,
87 identity_custody,
88 store_dir.clone(),
89 );
90 let cloud_storage = StoreCloudStorage::new(
91 security.clone(),
92 cloud_homes,
93 credentials,
94 clock,
95 cloudkit_ops,
96 blob_chunking,
97 );
98 let blob_cache = StoreBlobCache::new(database.clone(), store_dir.clone());
99 let local_blob_access =
100 LocalStoreBlobAccess::new(database.clone(), store_dir.clone(), blob_cache);
101 let blob_access = StoreBlobAccess::new(
102 database.clone(),
103 config_provider,
104 cloud_storage,
105 local_blob_access.clone(),
106 );
107 let blobs = StoreBlobs::new(database.clone(), blob_access, local_blob_access);
108 Self {
109 reads,
110 blobs,
111 security,
112 }
113 }
114
115 /// Run a pure read against the connection coven owns and await the result.
116 ///
117 /// This is the read handle's form of
118 /// [`CovenHandle::read`](crate::CovenHandle::read): the closure receives
119 /// the same [`SqlReadContext`](crate::SqlReadContext), and Coven runs the
120 /// query in one transaction on an available read-only worker.
121 /// Attach [`process`](crate::Read::process) before awaiting to process owned
122 /// results on separate workers after releasing the connection.
123 pub fn read<F, R>(&self, f: F) -> crate::Read<'_, F>
124 where
125 F: for<'connection> FnOnce(crate::SqlReadContext<'connection>) -> CovenResult<R>
126 + Send
127 + 'static,
128 R: Send + 'static,
129 {
130 crate::Read::new(&self.reads, f)
131 }
132
133 /// Capture the exact current blob-bearing row version from this reader's
134 /// database snapshot.
135 pub async fn row_blob_ref(
136 &self,
137 table: &str,
138 row_id: &str,
139 ) -> Result<RowBlobRef, coven_database::DbError> {
140 self.blobs.row_blob_ref(table, row_id).await
141 }
142
143 /// Read a blob's whole plaintext through coven's locality-aware read: served from
144 /// the user's file (Local user-provided), coven's local store (Local
145 /// host-provided), the pinned/evictable cache on a Remote hit, or fetched from
146 /// the cloud into the cache on a Remote miss. The read counterpart of
147 /// [`CovenHandle::read_blob`](crate::CovenHandle::read_blob).
148 ///
149 /// A cloud fetch writes the fetched bytes into the per-device cache
150 /// (`storage/cache/`) with an atomic temp-then-rename — device scratch, no synced
151 /// state touched — so a File Provider materializing remote content works through a
152 /// read-only handle. The supplied [`RowBlobRef`] already carries the exact stored
153 /// object and authority, so the read performs no database write or cloud listing.
154 pub async fn read_blob(&self, blob: &RowBlobRef) -> Result<Vec<u8>, BlobCacheError> {
155 self.blobs.read(blob).await
156 }
157
158 /// Open an exact row blob's plaintext for ranged reading, for streaming or
159 /// seeking without loading the whole file. The ranged sibling of
160 /// [`read_blob`](Self::read_blob); the read counterpart of
161 /// [`CovenHandle::open_blob_stream`](crate::CovenHandle::open_blob_stream).
162 ///
163 /// Opening resolves the blob's locality, proves the plaintext's size and content
164 /// hash against the row, and holds the open file, so every
165 /// [`BlobStream::read_at`] costs only the bytes it returns. A Remote miss fetches
166 /// the exact cloud object once and populates the per-device cache
167 /// (`storage/cache/`) with an atomic temp-then-link — device scratch, no synced
168 /// state touched — so a File Provider serving ranges through a read-only handle
169 /// downloads the object once per opened stream, not once per range.
170 pub async fn open_blob_stream(&self, blob: &RowBlobRef) -> Result<BlobStream, BlobCacheError> {
171 self.blobs.open_stream(blob).await
172 }
173
174 /// Open a payload
175 /// [`CovenHandle::seal_app_data`](crate::CovenHandle::seal_app_data) produced,
176 /// resolving the store's master keyring through this handle's custody. The read
177 /// side of app-data sealing: a secondary reader opens what the writer sealed,
178 /// under whichever generation the payload names.
179 ///
180 /// There is no seal counterpart here — sealing writes new ciphertext, which is
181 /// the writer's job; this handle only reads.
182 ///
183 /// [`SealError::Locked`] if the store is locked; a wrong `aad`, a tampered
184 /// payload, an unreadable version, or a generation this store's keyring lacks
185 /// each surface their own typed error.
186 pub fn open_app_data(&self, sealed: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
187 self.security.open_app_data(sealed, aad)
188 }
189
190 /// Whether every blob in `blobs` is pinned for offline — present in coven's kept
191 /// cache folder (`storage/pinned/`). An empty set is vacuously pinned. A read; it
192 /// stats the folder, never writes.
193 pub async fn is_pinned(&self, blobs: &[RowBlobRef]) -> Result<bool, BlobCacheError> {
194 self.blobs.all_pinned(blobs).await
195 }
196}