Skip to main content

coven/
handle.rs

1//! Application calls delegate to database, custody, operation and file owners.
2
3#[path = "file.rs"]
4mod file;
5
6#[path = "storage_credentials.rs"]
7mod credentials;
8
9#[path = "handle_sync.rs"]
10mod sync_control;
11
12#[path = "handle_sync_status.rs"]
13mod sync_status;
14
15#[path = "member_keys.rs"]
16mod member_keys;
17
18#[path = "handle_delete.rs"]
19mod deletion;
20pub use deletion::IfUnsent;
21
22#[path = "handle_folders.rs"]
23mod folders;
24
25#[path = "handle_file_cleanup.rs"]
26mod file_cleanup;
27
28#[path = "handle_discarded_devices.rs"]
29mod discarded_devices;
30
31#[path = "handle_local_bytes.rs"]
32mod local_bytes;
33
34#[path = "handle_device_only.rs"]
35mod device_only;
36
37#[path = "handle_transfers.rs"]
38mod transfers;
39
40#[path = "handle_uploads.rs"]
41mod uploads;
42
43#[path = "handle_writes.rs"]
44mod writes;
45
46#[path = "circles.rs"]
47mod circles;
48pub use circles::Circles;
49
50use crate::*;
51use coven_crypto::custody::{StoreCustody, StoreKeychain};
52use coven_database::Database;
53use coven_foundation::locks::lock_unpoisoned;
54use std::sync::{Arc, Mutex};
55
56/// A shared handle to the open store and its running work (Appendix F.4).
57/// Administrative changes catch up with storage before starting and report
58/// a refusal by the applied state as `SyncError::Rejected`, including its
59/// typed target.
60#[derive(Clone)]
61pub struct CovenHandle {
62    // Drops first, so the store is closing before this clone of the
63    // database goes.
64    _scope: Arc<HandleScope>,
65    database: Database,
66    files: coven_sync::Files,
67    /// The store's operation steps, which an app's write runs.
68    steps: Arc<coven_sync::OperationSteps>,
69    waits: Arc<coven_sync::RetryWaits>,
70    state: Arc<coven_sync::SequenceState>,
71    /// The store's restore codes and recorded location.
72    codes: coven_sync::RestoreCodes,
73    /// Follows the store's watch of the device's network (§6.7).
74    network: Arc<coven_sync::NetworkFollow>,
75    storage: Arc<crate::storage::StorageConnections>,
76    custody: Arc<Mutex<Option<StoreCustody>>>,
77    directory: StoreDir,
78    keychain: Arc<StoreKeychain>,
79    device: DeviceId,
80}
81
82/// The app's hold on the store: ending it, as the last clone of the handle
83/// drops, marks the store closing though workers still hold the database
84/// (F.4).
85struct HandleScope(Database);
86
87impl Drop for HandleScope {
88    fn drop(&mut self) {
89        self.0.begin_close();
90    }
91}
92
93impl std::fmt::Debug for CovenHandle {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("CovenHandle").finish_non_exhaustive()
96    }
97}
98
99impl CovenHandle {
100    pub(crate) fn new(
101        database: Database,
102        custody: StoreCustody,
103        files: coven_sync::Files,
104        steps: Arc<coven_sync::OperationSteps>,
105        waits: Arc<coven_sync::RetryWaits>,
106        state: Arc<coven_sync::SequenceState>,
107        codes: coven_sync::RestoreCodes,
108        network: Arc<coven_sync::NetworkFollow>,
109        storage: Arc<crate::storage::StorageConnections>,
110        directory: StoreDir,
111        keychain: Arc<StoreKeychain>,
112        device: DeviceId,
113    ) -> Self {
114        Self {
115            _scope: Arc::new(HandleScope(database.clone())),
116            database,
117            files,
118            steps,
119            waits,
120            state,
121            codes,
122            network,
123            storage,
124            custody: Arc::new(Mutex::new(Some(custody))),
125            directory,
126            keychain,
127            device,
128        }
129    }
130
131    /// The store's stable id, including before storage is configured.
132    pub fn store_id(&self) -> StoreId {
133        self.directory.id()
134    }
135
136    /// The store's current name, from this device's committed state (E9).
137    pub async fn store_name(&self) -> CovenResult<String> {
138        self.database.store_name().await.map_err(CovenError::from)
139    }
140
141    /// The same name query, live (E4).
142    pub fn subscribe_store_name(&self) -> LiveQuery<String, CovenError> {
143        self.database.subscribe_store_name()
144    }
145
146    /// The new person's name the join's request carried, which a store a join
147    /// published keeps on this device until a write acknowledges it
148    /// (`CovenWrite::acknowledge_joined_invitee`). None for a store no join
149    /// published, for a recovery invite's, which carries no name, and once
150    /// acknowledged (§13.2).
151    pub async fn joined_invitee(&self) -> CovenResult<Option<String>> {
152        self.database.joined_invitee().await.map_err(CovenError::from)
153    }
154
155    /// Renames the store, in a write of its own (`CovenWrite::rename_store`).
156    pub async fn rename_store(&self, name: &str) -> Result<Option<OperationId>, SyncError> {
157        let name = name.to_owned();
158        self.write(move |write| write.rename_store(&name)).await
159    }
160
161    /// This installation's device id, available before setup and retained after close.
162    pub fn device_id(&self) -> DeviceId {
163        self.device
164    }
165
166    /// The committed storage location, without credentials or provider requests.
167    /// None before setup; once set up, it stays. A closed handle refuses the call.
168    pub async fn storage_config(&self) -> Result<Option<StorageConfig>, SyncError> {
169        self.storage.storage_config().await
170    }
171
172    /// How many file uploads and file downloads run at once. Each upload checks the
173    /// upload limit as it starts; a pin captures the download limit.
174    pub fn transfer_limits(&self) -> TransferLimits {
175        self.files.transfer_limits()
176    }
177    /// Set limits for future transfers; transfers already running continue.
178    pub fn set_transfer_limits(&self, limits: TransferLimits) {
179        self.files.set_transfer_limits(limits);
180    }
181
182    /// Check S3 safety and provider operations, then set up S3 using this
183    /// member's own access key. The returned location is the recorded one,
184    /// carrying detected settings when the endpoint needed them.
185    /// `on_progress` is called on this task with each step as it runs (E1).
186    pub async fn setup_s3_storage(
187        &self,
188        storage: StorageConfig,
189        device_name: &str,
190        access_key_id: String,
191        secret_access_key: SecretText,
192        on_progress: impl Fn(Progress<StorageSetupStep>),
193    ) -> Result<StorageSetup, StorageSetupError> {
194        let (progress, watched) = tokio::sync::watch::channel(None);
195        let setup = self.setup_s3_reporting(
196            storage,
197            device_name,
198            access_key_id,
199            secret_access_key,
200            progress,
201        );
202        coven_foundation::progress::forward(watched, setup, on_progress).await
203    }
204
205    /// The S3 setup [`Self::setup_s3_storage`] runs, reporting its steps
206    /// into `progress` as it runs them.
207    pub(crate) async fn setup_s3_reporting(
208        &self,
209        storage: StorageConfig,
210        device_name: &str,
211        access_key_id: String,
212        secret_access_key: SecretText,
213        progress: crate::storage::SetupProgress,
214    ) -> Result<StorageSetup, StorageSetupError> {
215        self.storage
216            .setup_s3(storage, device_name, access_key_id, secret_access_key, progress)
217            .await
218    }
219    /// Probe the S3 service at this location with this member's key (§20.7),
220    /// writing only fresh objects under the store's `.coven-check` prefix and
221    /// deleting them, also on failure, then removing what earlier recorded
222    /// detection runs at that bucket and prefix left. Commits only its run's
223    /// record, kept until its probes and uploads are confirmed gone.
224    pub async fn detect_s3_settings(
225        &self,
226        storage: StorageConfig,
227        access_key_id: String,
228        secret_access_key: SecretText,
229    ) -> Result<S3Detection, StorageSetupError> {
230        self.storage.detect_s3(storage, access_key_id, secret_access_key).await
231    }
232    /// Present provider sign-in and keep tokens in coven's session custody.
233    /// Call setup to attach the chosen account to storage and persist credentials.
234    /// Dropping the future cancels sign-in without replacing the previous tokens.
235    pub async fn authenticate(&self, access: OAuthAccess) -> Result<(), StorageSetupError> {
236        self.storage.authenticate(access).await
237    }
238    /// Check provider operations and set up OAuth storage using coven's held sign-in.
239    /// `on_progress` is called on this task with each step as it runs (E1).
240    pub async fn setup_oauth_storage(
241        &self,
242        storage: StorageConfig,
243        device_name: &str,
244        on_progress: impl Fn(Progress<StorageSetupStep>),
245    ) -> Result<StorageSetup, StorageSetupError> {
246        let (progress, watched) = tokio::sync::watch::channel(None);
247        let setup = self.storage.setup_oauth(storage, device_name, progress);
248        coven_foundation::progress::forward(watched, setup, on_progress).await
249    }
250    /// Set up native iCloud storage in the entitled container. Coven obtains the
251    /// owner and creates a custom zone; reconnects retain the recorded location.
252    /// Unsupported platforms return `StorageFailure::ProviderUnavailable`.
253    /// `on_progress` is called on this task with each step as it runs (E1).
254    pub async fn setup_cloudkit_storage(
255        &self,
256        container: &str,
257        device_name: &str,
258        on_progress: impl Fn(Progress<StorageSetupStep>),
259    ) -> Result<StorageSetup, StorageSetupError> {
260        let (progress, watched) = tokio::sync::watch::channel(None);
261        let setup = self.storage.setup_cloudkit(container, device_name, progress);
262        coven_foundation::progress::forward(watched, setup, on_progress).await
263    }
264    /// Cancel the storage setup running on this store (§11.6). Before its
265    /// point of no return setup undoes what it wrote, fails with
266    /// `StorageSetupError::Cancelled`, and `coven_operations` lists it
267    /// cancelled. Past that point, from its `CreatingStore` or
268    /// `RecordingAccess` step on or once a crash cut it there, it finishes,
269    /// and the answer says so. Holds no lock: setup holds its call's.
270    pub fn cancel_storage_setup(&self) -> crate::storage::SetupCancellation {
271        self.storage.cancel_setup()
272    }
273    /// Have the next setup stop or wait at one of its stops.
274    #[cfg(test)]
275    pub(crate) fn test_setup_hook(&self, hook: crate::storage::SetupHook) {
276        self.storage.test_setup_hook(hook);
277    }
278    /// Stop the next S3 key replacement after `at`, as a crash would.
279    #[cfg(test)]
280    pub(crate) fn test_replace_crash(&self, at: coven_sync::ReplaceStop) {
281        self.storage.test_replace_crash(at);
282    }
283    /// Open this member's sealed keys and connect without starting the loop.
284    pub async fn unlock_store_key(&self) -> Result<ConnectedStorage, StoreKeyUnlockError> {
285        self.storage.unlock().await
286    }
287    /// The first load of a store a join published once it found its
288    /// admission, reporting loading's steps to `progress` (§13.2).
289    pub(crate) async fn load_joined(
290        &self,
291        progress: tokio::sync::watch::Sender<Option<Progress<coven_database::LoadStep>>>,
292    ) -> Result<(), SyncError> {
293        self.storage.load_joined(progress).await
294    }
295    /// Whether custody holds opened store keys.
296    pub fn store_key_state(&self) -> Result<StoreKeyState, KeyError> {
297        self.storage.key_state()
298    }
299    /// The latest completed replacement stored in this installation, or None.
300    /// It survives reopening; unsent edits of the old copy were discarded (§19.3).
301    pub async fn device_reset(&self) -> CovenResult<Option<DeviceReset>> {
302        self.database.device_reset().await.map_err(CovenError::from)
303    }
304
305    /// The same pre-built query, including a replacement completed before opening.
306    pub fn subscribe_device_reset(&self) -> LiveQuery<Option<DeviceReset>, CovenError> {
307        self.database.subscribe_device_reset()
308    }
309
310    /// Current local conditions, including while disconnected, each with
311    /// the cause `coven_pending.cause` keeps beside it.
312    pub async fn pending(&self) -> CovenResult<Vec<PendingRecord>> {
313        self.database.pending().await.map_err(CovenError::from)
314    }
315
316    /// The members and their devices, as this device's applied slots have them (E9).
317    pub async fn members(&self) -> CovenResult<Vec<MemberInfo>> {
318        self.database.members().await.map_err(CovenError::from)
319    }
320
321    /// The same internal query, live (E4).
322    pub fn subscribe_members(&self) -> LiveQuery<Vec<MemberInfo>, CovenError> {
323        self.database.subscribe_members()
324    }
325
326    /// Changes a member's role, in a write of its own
327    /// (`CovenWrite::set_member_role`).
328    pub async fn set_member_role(
329        &self,
330        member: &MemberId,
331        role: MemberRole,
332    ) -> Result<Option<OperationId>, SyncError> {
333        let member = *member;
334        self.write(move |write| write.set_member_role(&member, role)).await
335    }
336    /// Removes a member, in a write of its own (`CovenWrite::remove_member`).
337    pub async fn remove_member(&self, member: &MemberId) -> Result<Option<OperationId>, SyncError> {
338        let member = *member;
339        self.write(move |write| write.remove_member(&member)).await
340    }
341    /// Removes a device, in a write of its own (`CovenWrite::remove_device`).
342    pub async fn remove_device(
343        &self,
344        device: DeviceId,
345    ) -> Result<(Option<OperationId>, Option<ProviderSignOut>), SyncError> {
346        self.write(move |write| write.remove_device(device)).await
347    }
348    /// The person did the provider step for a lost device of the account
349    /// that holds the store (§14.2): signed it out, so nothing it sent can
350    /// land any more. This device takes back every account that device's
351    /// invite shares named which no member or open invite uses, with
352    /// requests like any other revocation, which run after this call
353    /// returns. Needs the connection (`NoStorage` without it), and is refused
354    /// with `CutOffRefused` naming why: `NoAccountShared` on S3, `NotStoreOwner`
355    /// on a device that isn't of the member whose account holds the store (see
356    /// `holds_store`), and `DeviceNotGone` for a device not gone for good,
357    /// which a later pass may change. While a request to one of those accounts runs it fails with
358    /// `DbError::ProviderRequestRunning` and changes nothing: call again
359    /// once it ends (§20.9).
360    pub async fn confirm_device_cut_off(&self, device: DeviceId) -> Result<(), SyncError> {
361        self.storage.confirm_device_cut_off(device).await
362    }
363    /// Whether this device's member reaches storage through the account
364    /// that holds the store (§20.9): only their devices share it and take
365    /// sharing back, so only they can confirm a device cut off. False on S3,
366    /// where no account holds the store. Reads the sequence forward first;
367    /// needs the connection (`NoStorage` without it).
368    pub async fn holds_store(&self) -> Result<bool, SyncError> {
369        self.storage.holds_store().await
370    }
371    /// Waits for an operation the app's writes journaled to end and returns
372    /// its outcome, as `coven_operations` lists it (§11.4, E6); the outcome
373    /// stays listed until a write acknowledges it. It waits for storage
374    /// too: a pass runs the operation once storage is connected. An
375    /// operation not listed fails with `OperationUnknown`.
376    pub async fn operation_outcome(
377        &self,
378        operation: OperationId,
379    ) -> Result<OperationOutcome, SyncError> {
380        let mut listed: LiveQuery<_, SyncError> = self.database.subscribe(move |sql| {
381            Ok(sql
382                .query(
383                    "SELECT outcome FROM coven_operations WHERE operation=?1",
384                    [operation.0],
385                    |row| row.get::<_, Option<OperationOutcome>>(0),
386                )?
387                .pop())
388        });
389        while let Some(listed) = listed.next().await {
390            match listed? {
391                None => return Err(DbError::OperationUnknown(operation).into()),
392                Some(Some(outcome)) => return Ok(outcome),
393                Some(None) => {}
394            }
395        }
396        Err(DbError::StoreClosed.into())
397    }
398    /// The bucket, store prefix and filled member policy for making a key in
399    /// the provider's console (§20.10).
400    pub async fn guided_access_key(&self) -> Result<GuidedAccessKey, SyncError> {
401        self.storage.guided_access_key().await
402    }
403    /// Reload this device from snapshots, retaining and replaying its waiting writes (§19.2).
404    pub async fn reload_from_snapshot(&self) -> Result<(), OperationError> {
405        // An operation's state runs to tens of KiB; boxed, the future the
406        // app holds stays small (application_api_tests bounds it). The other
407        // operations below are boxed for the same reason.
408        Box::pin(self.storage.reload_from_snapshot()).await
409    }
410    /// Reset the store audience from this device as an admin, then reload locally (§19.4).
411    pub async fn reset_store(&self) -> Result<(), SyncError> {
412        self.storage.reset(Audience::Store).await
413    }
414    /// Make an operation's retry or undo due now, from its recorded
415    /// progress, in a write of its own (`CovenWrite::retry_pending_operation`).
416    pub async fn retry_pending_operation(
417        &self,
418        operation: OperationId,
419    ) -> Result<(), OperationError> {
420        self.write(move |write| write.retry_pending_operation(operation)).await
421    }
422    /// Makes an invite, in a write of its own (`CovenWrite::create_invite`).
423    pub async fn create_invite(
424        &self,
425        target: InviteTarget,
426        access: InviteAccess,
427        lifetime: InviteLifetime,
428        inviter: String,
429    ) -> Result<(InviteId, MemberId, OperationId), SyncError> {
430        self.write(move |write| write.create_invite(target, access, lifetime, inviter)).await
431    }
432    /// Follow `coven_invites` with a live query, as an app does (E10).
433    #[cfg(any(test, feature = "test-utils"))]
434    pub fn test_invites(&self) -> coven_database::test_utils::Followed<Vec<Invite>, DbError> {
435        Invite::follow(&self.database)
436    }
437    /// Approves a request, in a write of its own
438    /// (`CovenWrite::approve_join_request`).
439    pub async fn approve_join_request(
440        &self,
441        request: &JoinRequest,
442    ) -> Result<OperationId, SyncError> {
443        let request = request.clone();
444        self.write(move |write| write.approve_join_request(&request)).await
445    }
446    /// Declines a request, in a write of its own
447    /// (`CovenWrite::decline_join_request`).
448    pub async fn decline_join_request(&self, request: &JoinRequest) -> Result<(), SyncError> {
449        let request = request.clone();
450        self.write(move |write| write.decline_join_request(&request)).await
451    }
452    /// Cancels an invite, in a write of its own (`CovenWrite::cancel_invite`).
453    pub async fn cancel_invite(&self, invite: &InviteId) -> Result<(), SyncError> {
454        let invite = *invite;
455        self.write(move |write| write.cancel_invite(&invite)).await
456    }
457    /// Circle calls borrowing this store's operation owner.
458    pub fn circles(&self) -> Circles<'_> {
459        Circles::new(self)
460    }
461
462    /// A read of one consistent snapshot, run when awaited. Attach `process`
463    /// to work on the result after the connection is released. It fails
464    /// with the closure's own error type, a database failure as its
465    /// `From<DbError>`.
466    pub fn read<F, R, E>(&self, read: F) -> Read<'_, F>
467    where
468        F: FnOnce(SqlReadContext<'_>) -> Result<R, E> + Send + 'static,
469        R: Send + 'static,
470        E: From<DbError> + task::CarriesBug + Send + 'static,
471    {
472        self.database.read(read)
473    }
474
475    /// A live query. Coven records the tables, columns and keys the query
476    /// reads, and reruns it only for writes that touch them.
477    pub fn subscribe<F, R, E>(&self, query: F) -> LiveQuery<R, E>
478    where
479        F: Fn(SqlReadContext<'_>) -> Result<R, E> + Send + Sync + 'static,
480        R: Send + 'static,
481        E: From<DbError> + task::CarriesBug + Send + 'static,
482    {
483        self.database.subscribe(query)
484    }
485
486    /// A live query whose request, such as a page or a search term, can be
487    /// replaced without starting a new subscription.
488    pub fn subscribe_reconfigurable<Q, F, R, E>(
489        &self,
490        initial_request: Q,
491        query: F,
492    ) -> ReconfigurableLiveQuery<Q, R, E>
493    where
494        Q: Clone + PartialEq + Send + Sync + 'static,
495        F: Fn(&Q, SqlReadContext<'_>) -> Result<R, E> + Send + Sync + 'static,
496        R: Send + 'static,
497        E: From<DbError> + task::CarriesBug + Send + 'static,
498    {
499        self.database.subscribe_reconfigurable(initial_request, query)
500    }
501
502    /// The SQLite settings the store's connections have in effect (§4).
503    pub async fn sqlite_settings(&self) -> Result<SqliteSettingsInEffect, DbError> {
504        self.database.sqlite_settings().await
505    }
506
507    /// Checkpoints the store's WAL between writes, returning what SQLite
508    /// reports (§4). `Restart` and `Truncate` wait up to the busy timeout for
509    /// reads still using the WAL, and report `busy` if they outlast it.
510    /// `NotStarted` when another connection was checkpointing or committing.
511    pub async fn checkpoint(&self, mode: CheckpointMode) -> Result<WalCheckpoint, DbError> {
512        self.database.checkpoint(mode).await
513    }
514
515    /// Gives back free pages of a store created with incremental auto-vacuum,
516    /// between writes (§4). On any other store it does nothing.
517    pub async fn incremental_vacuum(&self, pages: IncrementalVacuum) -> Result<(), DbError> {
518        self.database.incremental_vacuum(pages).await
519    }
520
521    /// Gives back the memory every connection's page cache holds, as SQLite's
522    /// `sqlite3_db_release_memory` does, for when the OS warns of low memory
523    /// or the app goes to the background (§4). SQLite keeps every
524    /// connection's unused pages in one pool for the process, so one release
525    /// on an idle reader frees them all. Safe at any time, and never waits
526    /// for a write. Later reads load the pages they need again.
527    pub async fn release_memory(&self) -> Result<(), DbError> {
528        self.database.release_memory().await
529    }
530
531    /// Every lost value and removed row, as `_coven_lost` holds them (§8.2, §8.4).
532    pub async fn lost_values(&self) -> CovenResult<Vec<LostValue>> {
533        self.database.lost_values().await.map_err(CovenError::from)
534    }
535
536    /// Dismisses lost values in a write of its own
537    /// (`CovenWrite::dismiss_lost_values`).
538    pub async fn dismiss_lost_values(&self, values: &[LostValue]) -> CovenResult<()> {
539        let values = values.to_vec();
540        self.write(move |write| Ok(write.dismiss_lost_values(&values)?)).await
541    }
542
543    /// The same, as a live query.
544    pub fn subscribe_lost_values(&self) -> LiveQuery<Vec<LostValue>, CovenError> {
545        self.database.subscribe_lost_values()
546    }
547
548    /// The file a row references, by the id the row holds: its size,
549    /// namespace, audience and place, read in one committed state. An id this
550    /// device has no record of returns `DbError::FileAbsent`; a malformed
551    /// record returns `DbError::DamagedDatabase`.
552    pub async fn file_ref(&self, file: FileId) -> Result<FileRef, DbError> {
553        self.database.file_ref(file).await
554    }
555
556    /// Reads a whole file. A stored file is checked against its content hash.
557    pub async fn read_file(&self, file: &FileRef) -> Result<Vec<u8>, FileReadError> {
558        let stream = self.open_file_stream(file).await?;
559        stream.read_at(0, stream.plaintext_size()).await
560    }
561
562    /// Opens a file for reading ranges (§17.7), as its record stands when it
563    /// opens; keep the stream for as long as the file is read. A file only on
564    /// another device fails with `FileReadError::DeviceOnly`. Its shared store
565    /// lock prevents deletion until it and its I/O finish.
566    pub async fn open_file_stream(&self, file: &FileRef) -> Result<FileStream, FileReadError> {
567        self.files.open_file_stream(file).await
568    }
569
570    /// The path, size and modification time coven recorded for a file's
571    /// user original on this device, or `None` when it has none.
572    pub async fn user_file(&self, file: FileId) -> Result<Option<UserFile>, DbError> {
573        self.database.user_file(file).await
574    }
575
576    /// Removes the store keys from key custody. A failed removal leaves the
577    /// custody failure visible to the caller; member identity is retained.
578    pub async fn forget_store_keys(&self) -> Result<(), KeyError> {
579        self.storage.forget_store_keys().await
580    }
581
582    /// Keeps an app secret, such as an API token, in the same keychain and
583    /// under the same access policy as coven's keys. Names are arbitrary strings.
584    /// Coven records the name in a keychain list before writing the value to its
585    /// own entry, retaining the platform's per-secret size limit. Deleting the
586    /// store removes every listed secret even if its database is damaged.
587    /// Failure may leave an extra name; retrying is safe.
588    pub fn set_host_secret(&self, name: &str, value: &str) -> Result<(), KeyError> {
589        lock_unpoisoned(&self.custody)
590            .as_ref()
591            .ok_or(KeyError::StoreClosed)?
592            .set_host_secret(name, value)
593    }
594    /// The secret, or `None` if it was never set.
595    pub fn host_secret(&self, name: &str) -> Result<Option<String>, KeyError> {
596        lock_unpoisoned(&self.custody).as_ref().ok_or(KeyError::StoreClosed)?.host_secret(name)
597    }
598    /// Deletes the secret before its recorded name; succeeds if absent.
599    /// Failure may leave an extra name; retrying is safe.
600    pub fn delete_host_secret(&self, name: &str) -> Result<(), KeyError> {
601        lock_unpoisoned(&self.custody)
602            .as_ref()
603            .ok_or(KeyError::StoreClosed)?
604            .delete_host_secret(name)
605    }
606
607    /// Stops operation and file work, closes connections and releases the writer
608    /// lock. Open file streams retain their shared deletion guards. Later
609    /// database calls on any clone fail with `DbError::StoreClosed`; custody
610    /// calls fail with `KeyError::StoreClosed`. Closing reports every failure:
611    /// one alone as itself, several chained as `SyncError::Cleanup`.
612    pub async fn close(&self) -> Result<(), SyncError> {
613        self.database.begin_close();
614        let handle = self.clone();
615        let bugs = self.database.bugs().clone();
616        let closing = async move {
617            // No network report reaches the store's waits once it closes.
618            handle.network.close().await;
619            let storage = handle.storage.close().await;
620            handle.files.close().await;
621            let custody = handle.custody.clone();
622            let bugs = handle.database.bugs().clone();
623            let custody = crate::task::blocking_completing("close custody", bugs, move || {
624                if let Some(custody) = lock_unpoisoned(&custody).take() {
625                    custody.close();
626                }
627                Ok::<_, std::convert::Infallible>(())
628            })
629            .await
630            .map(|closed| closed.unwrap_or_else(|never| match never {}));
631            let database = handle.database.close().await;
632            // Closing the connection can fail while releasing its provider.
633            // Finish closing every owner and erase custody before reporting.
634            let storage = match storage {
635                Err(SyncError::Database(DbError::StoreClosed)) => Ok(()),
636                result => result,
637            };
638            [storage, custody.map_err(SyncError::from), database.map_err(SyncError::from)]
639                .into_iter()
640                .filter_map(Result::err)
641                .reduce(SyncError::cleanup)
642                .map_or(Ok(()), Err)
643        };
644        crate::task::spawn_completing("close the store", bugs, closing).await?
645    }
646    /// The last slot this installation applied.
647    #[cfg(test)]
648    pub(crate) async fn applied_slot(&self) -> u64 {
649        self.database.applied_slot().await.unwrap()
650    }
651    /// Membership, keys and audience state as this installation applied them.
652    #[cfg(test)]
653    pub(crate) async fn store_state(&self) -> coven_database::StoreState {
654        self.database.store_state().await.unwrap()
655    }
656    /// Wait until the network follower, then the connection attempt, the
657    /// sync loop and the upload worker, those running, have each seen what
658    /// happened before this call, and what the ones before them did, and
659    /// wait.
660    /// Wait until a call finds the storage call lock held and waits, after
661    /// this call.
662    #[cfg(test)]
663    pub(crate) fn test_call_waits(&self) -> impl std::future::Future<Output = ()> {
664        self.storage.call_waits_from_now()
665    }
666    /// Wait until downloads have found the store's download limit full
667    /// `count` more times than before this call.
668    #[cfg(test)]
669    pub(crate) fn test_downloads_waiting(
670        &self,
671        count: usize,
672    ) -> impl std::future::Future<Output = ()> {
673        self.storage.downloads_waiting_from_now(count)
674    }
675    /// Follow the subject's persisted retry wait as failures arm it.
676    #[cfg(test)]
677    pub(crate) fn test_follow_retry_wait(
678        &self,
679        subject: coven_database::RetrySubject,
680    ) -> coven_database::test_utils::Followed<Option<coven_database::RetryWait>, DbError> {
681        coven_database::test_utils::Followed::new(self.database.subscribe_retry_wait(subject))
682    }
683    /// Follow membership and audience state as passes apply it.
684    #[cfg(test)]
685    pub(crate) fn test_store_state(
686        &self,
687    ) -> coven_database::test_utils::Followed<coven_database::StoreState, DbError> {
688        coven_database::test_utils::Followed::new(self.database.subscribe_store_state())
689    }
690    /// Wait until a pause is asked of the sync loop after this call.
691    #[cfg(test)]
692    pub(crate) fn test_pause_asked(&self) -> impl std::future::Future<Output = ()> {
693        self.storage.pause_asked_from_now()
694    }
695    #[cfg(test)]
696    pub(crate) async fn test_parked(&self) {
697        self.network.parked_from_now().await;
698        self.storage.parked().await;
699    }
700    #[cfg(test)]
701    pub(crate) async fn test_connection_retry_wait(&self) -> Option<coven_database::RetryWait> {
702        self.database.retry_wait(coven_database::RetrySubject::Sync).await.unwrap()
703    }
704    /// Record `record` as a failure saved before a process stopped, without
705    /// the status that shows it.
706    #[cfg(test)]
707    pub(crate) async fn test_record_pending(&self, record: coven_sync::PendingRecord) {
708        self.database.record_pending(record).await.unwrap();
709    }
710    /// Halt sync with `bug`, as the upload worker's panic does (E15).
711    #[cfg(test)]
712    pub(crate) async fn test_halt(&self, bug: crate::task::Bug) {
713        self.waits.sync_halted(bug).await.unwrap();
714    }
715
716    /// Panic in the next write the store's writer judges, as a bug in it
717    /// would.
718    #[cfg(test)]
719    pub(crate) async fn test_panic_in_next_write(&self, message: &'static str) {
720        self.database.test_panic_in_next_write(message).await.unwrap();
721    }
722
723    /// Count the work the store's connections run into `counter`, in
724    /// application tests: its reads' SQLite steps, the app's and coven's
725    /// narrowing apart. Install it between calls, before the measured one.
726    #[cfg(any(test, feature = "test-utils"))]
727    pub async fn test_track_work(&self, counter: crate::WorkCounter) -> Result<(), DbError> {
728        self.database.test_track_work(counter).await
729    }
730
731    /// The plaintext bytes each section of a store snapshot would hold now,
732    /// for a test measuring what a snapshot keeps (D7). Every section's place
733    /// is in `coven_format::snapshot::section`.
734    #[cfg(any(test, feature = "test-utils"))]
735    pub async fn test_snapshot_section_bytes(
736        &self,
737    ) -> Result<
738        [u64; coven_format::snapshot::SECTIONS],
739        coven_database::SnapshotWriteError<std::convert::Infallible>,
740    > {
741        let id = coven_format::admin::SnapshotId {
742            audience: coven_merge::Audience::Store,
743            device: self.device_id(),
744            number: 1,
745        };
746        self.database.test_snapshot_section_bytes(id).await
747    }
748
749    /// Read committed plaintext records still awaiting upload in application tests.
750    #[cfg(any(test, feature = "test-utils"))]
751    pub async fn test_queued_writes(
752        &self,
753    ) -> Result<Vec<coven_format::write::WriteRecord>, DbError> {
754        self.database.test_queued_writes().await
755    }
756}
757
758#[cfg(test)]
759impl CovenHandle {
760    /// Where the waiting upload of `file` sends its bytes, for tests that
761    /// fault that path.
762    pub(crate) async fn test_upload_location(&self, file: FileId) -> coven_database::FileLocation {
763        self.database
764            .upload_queue()
765            .await
766            .unwrap()
767            .uploads
768            .into_iter()
769            .find(|upload| upload.files.contains(&file))
770            .expect("an upload stores the file")
771            .location
772    }
773}
774
775#[cfg(test)]
776#[path = "handle_tests.rs"]
777mod tests;