Skip to main content

coven/
read_handle.rs

1//! The read-only application surface caches files under a shared deletion guard.
2
3use crate::*;
4use coven_database::{Database, ReadOnly};
5
6/// A handle that reads synced rows and maintains its local file cache (§4, E1).
7/// Its shared store lock prevents deletion until all its connections close.
8/// Its SQL may not read `coven_sync_status`: it can't tell whether the writer
9/// that last wrote the row still runs (E5).
10///
11/// ```compile_fail
12/// async fn no_writes(handle: &coven::CovenReadHandle) {
13///     handle.write(|_| Ok::<_, coven::CovenError>(())).await;
14/// }
15/// ```
16/// ```compile_fail
17/// fn no_live_query(handle: &coven::CovenReadHandle) {
18///     handle.subscribe(|_| Ok::<_, coven::CovenError>(()));
19/// }
20/// ```
21#[derive(Clone)]
22pub struct CovenReadHandle {
23    database: Database<ReadOnly>,
24    files: coven_sync::Files<ReadOnly>,
25}
26
27impl CovenReadHandle {
28    pub(crate) fn new(database: Database<ReadOnly>, files: coven_sync::Files<ReadOnly>) -> Self {
29        Self { database, files }
30    }
31    /// The store's current name from this device's committed state (E9).
32    pub async fn store_name(&self) -> CovenResult<String> {
33        self.database.store_name().await.map_err(CovenError::from)
34    }
35
36    /// A read of one consistent snapshot, run when awaited.
37    pub fn read<F, R, E>(&self, read: F) -> Read<'_, F, ReadOnly>
38    where
39        F: FnOnce(SqlReadContext<'_>) -> Result<R, E> + Send + 'static,
40        R: Send + 'static,
41        E: From<DbError> + task::CarriesBug + Send + 'static,
42    {
43        self.database.read(read)
44    }
45    /// The SQLite settings this handle's connections have in effect (§4).
46    pub async fn sqlite_settings(&self) -> Result<SqliteSettingsInEffect, DbError> {
47        self.database.sqlite_settings().await
48    }
49    /// The file a row references, by the id the row holds.
50    pub async fn file_ref(&self, file: FileId) -> Result<FileRef, DbError> {
51        self.database.file_ref(file).await
52    }
53    /// The path, size and modification time recorded for a file's original.
54    pub async fn user_file(&self, file: FileId) -> Result<Option<UserFile>, DbError> {
55        self.database.user_file(file).await
56    }
57    /// Reads a whole file. A stored file is checked against its content hash.
58    pub async fn read_file(&self, file: &FileRef) -> Result<Vec<u8>, FileReadError> {
59        let stream = self.open_file_stream(file).await?;
60        stream.read_at(0, stream.plaintext_size()).await
61    }
62    /// Opens a file for reading ranges (§17.7), as its record stands when it
63    /// opens; keep the stream for as long as the file is read. Its shared
64    /// store lock prevents deletion until it and its I/O finish.
65    pub async fn open_file_stream(
66        &self,
67        file: &FileRef,
68    ) -> Result<FileStream<ReadOnly>, FileReadError> {
69        self.files.open_file_stream(file).await
70    }
71    /// Closes read and cache connections on all clones and releases their
72    /// storage client and shared store lock, while the app still holds the
73    /// handle. From its first poll every clone refuses new calls with
74    /// `StoreClosed`; the closing itself runs as its own task, so dropping
75    /// this future doesn't stop it (E1, E15). File streams retain their own
76    /// locks.
77    pub async fn close(&self) -> Result<(), DbError> {
78        self.database.begin_close();
79        let handle = self.clone();
80        let bugs = self.database.bugs().clone();
81        let closing = async move {
82            handle.files.close().await;
83            handle.database.close().await
84        };
85        crate::task::spawn_completing("close the read-only store", bugs, closing).await?
86    }
87}