1use tracing::{debug, info, warn};
12
13use crate::blob::DrainOutcome;
14use coven_foundation::changeset::RowChange;
15use coven_foundation::store_dir::StoreDir;
16use coven_protocol::blob::BlobTransitionObserver;
17
18use super::status::DeviceActivity;
19use super::store::HeldStorePosition;
20use super::store::{AuthorizedWriterOperation, Store};
21use coven_foundation::stage_timing::StageTimings;
22use coven_protocol::objects::RotationPending;
23use coven_storage::{
24 BlobPathScheme, CloudSyncCipherStateAccess, CloudSyncConnection, CloudSyncObjectStorage,
25 CloudSyncRotationStateAccess,
26};
27
28#[derive(Debug)]
30pub struct SyncCycleResult {
31 pub changesets_applied: u64,
33 pub held_positions: Vec<HeldStorePosition>,
38 pub device_activity: Vec<DeviceActivity>,
42 pub sync_time: String,
44 pub local_blob_cleanup_pending: bool,
49 pub row_changes: Vec<RowChange>,
51 pub resume_drain_promptly: bool,
56 pub rotation_pending: Option<RotationPending>,
61}
62
63#[derive(Debug)]
64pub struct SyncCycleFailure {
65 kind: SyncCycleFailureKind,
66 operation: &'static str,
67 cause: Box<SyncCycleCause>,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71enum SyncCycleFailureKind {
72 Offline,
73 Failed,
74}
75
76impl SyncCycleFailure {
77 pub(crate) fn operation<E>(operation: &'static str, error: E) -> Self
78 where
79 E: Into<SyncCycleCause>,
80 {
81 let cause = error.into();
82 let kind = if super::error::error_chain_contains_transport(&cause) {
83 SyncCycleFailureKind::Offline
84 } else {
85 SyncCycleFailureKind::Failed
86 };
87 Self {
88 kind,
89 operation,
90 cause: Box::new(cause),
91 }
92 }
93
94 pub(crate) fn is_offline(&self) -> bool {
95 self.kind == SyncCycleFailureKind::Offline
96 }
97
98 fn concurrent(first: Self, second: Self) -> Self {
99 let kind = if first.is_offline() || second.is_offline() {
100 SyncCycleFailureKind::Offline
101 } else {
102 SyncCycleFailureKind::Failed
103 };
104 Self {
105 kind,
106 operation: "run Store publication and blob upload lanes",
107 cause: Box::new(SyncCycleCause::Concurrent {
108 first: Box::new(first),
109 second: Box::new(second),
110 }),
111 }
112 }
113
114 #[cfg(test)]
115 pub(crate) fn contains(&self, pattern: &str) -> bool {
116 self.to_string().contains(pattern)
117 }
118}
119
120impl std::fmt::Display for SyncCycleFailure {
121 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 write!(formatter, "{}: {}", self.operation, self.cause)
123 }
124}
125
126impl std::error::Error for SyncCycleFailure {
127 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
128 Some(self.cause.as_ref())
129 }
130}
131
132#[derive(Debug, thiserror::Error)]
133pub(crate) enum SyncCycleCause {
134 #[error("{first}; concurrently, {second}")]
135 Concurrent {
136 first: Box<SyncCycleFailure>,
137 second: Box<SyncCycleFailure>,
138 },
139 #[error("{0}")]
140 Database(#[from] coven_database::DbError),
141 #[error("{0}")]
142 Store(#[from] super::store::StoreError),
143 #[error("{0}")]
144 Registration(#[from] super::store::StoreRegistrationError),
145 #[error("{0}")]
146 Initialization(#[from] super::store::StoreInitializationError),
147 #[error("{0}")]
148 Circle(#[from] super::store::CircleOperationError),
149 #[error("{0}")]
150 DeviceExclusion(#[from] super::store::StoreDeviceExclusionError),
151 #[error("{0}")]
152 Reclaim(#[from] super::store::StoreReclaimError),
153 #[error("{0}")]
154 DeviceJoin(#[from] super::store::DeviceJoinError),
155 #[error("{0}")]
156 TombstoneDrain(#[from] crate::blob::delete::TombstoneDrainError),
157 #[error("{0}")]
158 TombstoneGc(#[from] super::store::commit_publication::operation::TombstoneGcError),
159 #[error("{0}")]
160 UploadFailures(#[from] crate::blob::UploadFailures),
161 #[error("{0}")]
162 WriterAuthorization(
163 #[from] super::store::commit_publication::operation::StoreWriterAuthorizationError,
164 ),
165 #[error("{0}")]
166 Acknowledgement(#[from] super::store::acknowledgements::StoreAckError),
167 #[error("{0}")]
168 Membership(#[from] super::store::MembershipOpsError),
169 #[error("{0}")]
170 Pull(#[from] super::store::StorePullError),
171 #[error("{0}")]
172 AuthorizationRefresh(
173 #[from] super::store::commit_publication::operation::AuthorizationRefreshError,
174 ),
175 #[error("{0}")]
176 PublishedBlobDrop(#[from] super::store::blob::PublishedBlobDropError),
177 #[error("{0}")]
178 StoreProtocol(#[from] coven_protocol::store_commit::StoreProtocolError),
179 #[error("{0}")]
180 RowRoutingKey(#[from] coven_protocol::circle::RowRoutingKeyError),
181 #[error("{0}")]
182 Snapshot(#[from] super::store::snapshots::SnapshotError),
183}
184
185#[cfg(test)]
186mod sync_cycle_failure_tests {
187 use super::*;
188
189 #[test]
190 fn registration_transport_source_is_offline() {
191 let error = crate::sync::store::StoreRegistrationError::Object(
192 coven_protocol::objects::StoreObjectError::Storage(
193 coven_protocol::objects::StorageError::Storage("provider unavailable".to_string()),
194 ),
195 );
196
197 let object = std::error::Error::source(&error).expect("object source");
198 assert!(object
199 .downcast_ref::<coven_protocol::objects::StoreObjectError>()
200 .is_some());
201 let storage = object.source().expect("storage source");
202 assert!(storage
203 .downcast_ref::<coven_protocol::objects::StorageError>()
204 .is_some());
205
206 assert!(SyncCycleFailure::operation("register", error).is_offline());
207 }
208
209 #[test]
210 fn registration_configuration_source_is_failed() {
211 let error = crate::sync::store::StoreRegistrationError::Object(
212 coven_protocol::objects::StoreObjectError::Storage(
213 coven_protocol::objects::StorageError::Configuration("missing bucket".to_string()),
214 ),
215 );
216
217 assert!(!SyncCycleFailure::operation("register", error).is_offline());
218 }
219}
220
221struct PreparedCycle {
222 sync_time: String,
223 resume_drain_promptly: bool,
224 rotation_pending: Option<RotationPending>,
225}
226
227struct CompletedPullCycle {
228 store_pull: super::store::StorePullResult,
229 local_blob_cleanup_pending: bool,
230 sync_time: String,
231 resume_drain_promptly: bool,
232 rotation_pending: Option<RotationPending>,
233}
234
235struct AuthorizedSyncCycle<'cycle, 'store> {
236 device_id: &'cycle str,
237 clock: &'cycle dyn coven_foundation::clock::Clock,
238 cipher: &'cycle dyn CloudSyncCipherStateAccess,
239 pending_rotation: &'cycle dyn CloudSyncRotationStateAccess,
240 master_keys: Option<&'cycle dyn coven_keys::keys::MasterKeyCustody>,
241 routing_encryption: Option<&'cycle coven_keys::encryption::EncryptionService>,
242 local_blob_access: &'cycle super::store::blob::LocalStoreBlobAccess,
243 observer: Option<&'cycle dyn BlobTransitionObserver>,
244 settled: &'cycle super::store::SettledCycle,
245 authorization: AuthorizedWriterOperation<'store>,
246}
247
248impl AuthorizedSyncCycle<'_, '_> {
249 async fn run(mut self) -> Result<SyncCycleResult, SyncCycleFailure> {
250 let mut timings =
253 StageTimings::counting("sync cycle", self.authorization.provider_requests());
254 let outcome = Box::pin(self.run_stages(&mut timings)).await;
255 timings.report();
256 outcome
257 }
258
259 async fn run_stages(
260 &mut self,
261 timings: &mut StageTimings,
262 ) -> Result<SyncCycleResult, SyncCycleFailure> {
263 timings
264 .stage(
265 "resume operations",
266 self.authorization
267 .resume_operations(self.routing_encryption),
268 )
269 .await?;
270 let prepared = Box::pin(self.prepare_before_pull(timings)).await?;
271 let store_pull = timings
272 .stage("pull", self.authorization.pull(self.routing_encryption))
273 .await?;
274 let completed = Box::pin(self.complete_after_pull(prepared, store_pull, timings)).await?;
275 if completed.rotation_pending.is_none() {
276 timings
277 .stage(
278 "publish epoch-close responses",
279 self.authorization
280 .circles()
281 .publish_circle_epoch_close_responses(),
282 )
283 .await
284 .map_err(|error| {
285 SyncCycleFailure::operation("publish Circle epoch-close responses", error)
286 })?;
287 if let Some(routing_encryption) = self.routing_encryption {
288 timings
289 .stage(
290 "finalize epoch closes",
291 self.authorization
292 .circles()
293 .finalize_ready_circle_epoch_closes(
294 &completed.sync_time,
295 routing_encryption,
296 ),
297 )
298 .await
299 .map_err(|error| {
300 SyncCycleFailure::operation("finalize Circle epoch closes", error)
301 })?;
302 }
303 let routing_encryption = self.routing_encryption;
304 timings
305 .stage(
306 "advance replay baseline",
307 Box::pin(self.stand_on_acknowledged_snapshot(routing_encryption)),
308 )
309 .await?;
310 timings
311 .stage(
312 "publish acknowledgements",
313 Box::pin(
314 self.authorization
315 .acknowledgements()
316 .stage_and_publish(&completed.sync_time, self.settled),
317 ),
318 )
319 .await?;
320 timings
321 .stage(
322 "retire arrived device joins",
323 Box::pin(self.authorization.retire_arrived_device_joins()),
324 )
325 .await
326 .map(|retired| {
327 if retired > 0 {
328 info!(
329 retired,
330 "Device joins reached their arrival and were retired"
331 );
332 }
333 })
334 .map_err(|error| {
335 SyncCycleFailure::operation("retire arrived device joins", error)
336 })?;
337 timings
338 .stage("reclaim packages", Box::pin(self.reclaim_packages()))
339 .await?;
340 }
341 Ok(SyncCycleResult {
342 changesets_applied: completed.store_pull.changesets_applied,
343 held_positions: completed.store_pull.held_positions,
344 device_activity: super::status::other_device_activity(
345 &completed.store_pull.visible_heads,
346 self.device_id,
347 ),
348 sync_time: completed.sync_time,
349 local_blob_cleanup_pending: completed.local_blob_cleanup_pending,
350 row_changes: completed.store_pull.row_changes,
351 resume_drain_promptly: completed.resume_drain_promptly,
352 rotation_pending: completed.rotation_pending,
353 })
354 }
355
356 async fn prepare_before_pull(
357 &mut self,
358 timings: &mut StageTimings,
359 ) -> Result<PreparedCycle, SyncCycleFailure> {
360 timings
370 .stage(
371 "refresh authorization",
372 self.authorization.refresh_authorization_state(
373 self.cipher,
374 self.pending_rotation,
375 self.master_keys,
376 ),
377 )
378 .await?;
379
380 let rotation_pending = self
391 .pending_rotation
392 .check(self.cipher.current_generation())
393 .err();
394 if let Some(pending) = &rotation_pending {
395 warn!(
396 rotation_state = ?pending.state,
397 live_generation = pending.live_generation,
398 "sync paused: store-key rotation work is incomplete; sealing nothing new for the cloud"
399 );
400 }
401
402 if rotation_pending.is_none() {
403 let drained = timings
404 .stage(
405 "drain tombstones",
406 self.authorization.drain_tombstones(self.clock),
407 )
408 .await
409 .map_err(|error| {
410 SyncCycleFailure::operation("drain queued blob tombstones", error)
411 })?;
412 if drained > 0 {
413 info!(count = drained, "Drained blob tombstones");
414 }
415 }
416 let reclaimed = timings
417 .stage(
418 "collect tombstones",
419 self.authorization.gc_tombstones(self.clock),
420 )
421 .await
422 .map_err(|error| {
423 SyncCycleFailure::operation("garbage-collect blob tombstones", error)
424 })?;
425 if reclaimed > 0 {
426 info!(count = reclaimed, "Reclaimed tombstoned blobs");
427 }
428
429 let local_seq = timings
430 .stage(
431 "read local position",
432 self.authorization.latest_local_store_position(),
433 )
434 .await
435 .map_err(|error| SyncCycleFailure::operation("read local Store position", error))?
436 .map_or(0, |reference| reference.coord.sequence());
437 timings
438 .stage(
439 "drain blob drop intents",
440 self.local_blob_access
441 .drain_published_blob_drop_intents(local_seq),
442 )
443 .await
444 .map_err(|error| {
445 SyncCycleFailure::operation("drain published blob drop intents", error)
446 })?;
447
448 let sync_time = self.clock.now().to_rfc3339();
452
453 let mut resume_drain_promptly = false;
454 if rotation_pending.is_none() {
455 let outcome = timings
456 .stage(
457 "drain blob uploads",
458 self.authorization.drain_uploads(
459 self.clock,
460 self.routing_encryption,
461 self.observer,
462 ),
463 )
464 .await
465 .map_err(|error| SyncCycleFailure::operation("drain queued blob uploads", error))?;
466 Self::record_upload_outcome(outcome, &mut resume_drain_promptly)?;
467 }
468
469 if rotation_pending.is_none() {
470 let published = timings
471 .stage(
472 "publish prepared writes",
473 self.authorization
474 .publish_prepared_store_writes(self.routing_encryption),
475 )
476 .await?;
477 if published > 0 {
478 info!(published, "Published queued Store writes");
479 }
480 }
481
482 Ok(PreparedCycle {
483 sync_time,
484 resume_drain_promptly,
485 rotation_pending,
486 })
487 }
488
489 async fn complete_after_pull(
490 &mut self,
491 prepared: PreparedCycle,
492 store_pull: super::store::StorePullResult,
493 timings: &mut StageTimings,
494 ) -> Result<CompletedPullCycle, SyncCycleFailure> {
495 let PreparedCycle {
496 sync_time,
497 mut resume_drain_promptly,
498 rotation_pending,
499 } = prepared;
500 if rotation_pending.is_none() {
501 let upload_authorization = self.authorization.blob_upload_lane();
506 let lanes = timings
507 .stage("publish pending writes and drain next blob root", async {
508 tokio::join!(
509 self.authorization
510 .publish_pending_store_writes(self.routing_encryption),
511 upload_authorization.drain_uploads(
512 self.clock,
513 self.routing_encryption,
514 self.observer,
515 ),
516 )
517 })
518 .await;
519 let (published, drained) = match lanes {
520 (Ok(published), Ok(drained)) => (published, drained),
521 (Err(first), Err(second)) => {
522 return Err(SyncCycleFailure::concurrent(
523 first,
524 SyncCycleFailure::operation("drain queued blob uploads", second),
525 ));
526 }
527 (Err(error), Ok(_)) => return Err(error),
528 (Ok(_), Err(error)) => {
529 return Err(SyncCycleFailure::operation(
530 "drain queued blob uploads",
531 error,
532 ));
533 }
534 };
535 if published > 0 {
536 info!(published, "Published Store writes");
537 }
538 Self::record_upload_outcome(drained, &mut resume_drain_promptly)?;
539 }
540
541 let local_seq = timings
542 .stage(
543 "read local position",
544 self.authorization.latest_local_store_position(),
545 )
546 .await
547 .map_err(|error| {
548 SyncCycleFailure::operation("read local Store position after publish", error)
549 })?
550 .map_or(0, |position| position.coord.sequence());
551 timings
552 .stage(
553 "drain blob drop intents",
554 self.local_blob_access
555 .drain_published_blob_drop_intents(local_seq),
556 )
557 .await
558 .map_err(|error| {
559 SyncCycleFailure::operation("drain published blob drop intents", error)
560 })?;
561 let local_blob_cleanup_pending = timings
562 .stage(
563 "drain local blob cleanup",
564 self.authorization.drain_local_blob_cleanup(),
565 )
566 .await
567 .map_err(|error| {
568 SyncCycleFailure::operation(
569 "drain local blob cleanup after Store publication",
570 error,
571 )
572 })?
573 || store_pull.local_blob_cleanup_pending;
574
575 timings
580 .stage(
581 "persist clock high-water",
582 self.authorization.persist_hlc_high_water(),
583 )
584 .await
585 .map_err(|error| SyncCycleFailure::operation("persist HLC high-water mark", error))?;
586
587 timings
588 .stage(
589 "publish snapshots",
590 self.authorization.snapshots().publish_due_snapshots(
591 &sync_time,
592 self.routing_encryption,
593 rotation_pending.is_some(),
594 ),
595 )
596 .await?;
597
598 Ok(CompletedPullCycle {
599 store_pull,
600 local_blob_cleanup_pending,
601 sync_time,
602 resume_drain_promptly,
603 rotation_pending,
604 })
605 }
606
607 fn record_upload_outcome(
608 outcome: DrainOutcome,
609 resume_drain_promptly: &mut bool,
610 ) -> Result<(), SyncCycleFailure> {
611 match outcome {
612 DrainOutcome::Drained {
613 uploaded,
614 yielded_for_publish,
615 failures,
616 } => {
617 if failures.has_transport_failure() {
618 return Err(SyncCycleFailure::operation("upload queued blobs", failures));
619 }
620 *resume_drain_promptly |= yielded_for_publish;
621 if uploaded > 0 {
622 info!(count = uploaded, "Drained blob uploads");
623 }
624 }
625 DrainOutcome::QueueEmpty => {}
626 DrainOutcome::AllInBackoff => {
627 debug!("Every queued blob upload is inside its retry backoff");
628 }
629 DrainOutcome::Paused => {
630 debug!("Blob uploads are paused by the host; nothing was admitted");
631 }
632 }
633 Ok(())
634 }
635
636 async fn stand_on_acknowledged_snapshot(
645 &mut self,
646 routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
647 ) -> Result<(), SyncCycleFailure> {
648 use super::store::{ReplayBaselineAdvance, ReplayBaselineDecline};
649
650 let outcome = self
651 .authorization
652 .acknowledgements()
653 .stand_on_acknowledged_snapshot(routing_encryption)
654 .await
655 .map_err(|error| {
656 SyncCycleFailure::operation("advance the Store replay baseline", error)
657 })?;
658 match outcome {
659 ReplayBaselineAdvance::Advanced(advanced) => info!(
660 commits = advanced.retired_commits,
661 pins = advanced.released_pins,
662 writes = advanced.folded_writes,
663 "Advanced the replay baseline over an acknowledged snapshot"
664 ),
665 ReplayBaselineAdvance::Declined(decline) => info!(
669 declined = decline.as_str(),
670 generation = decline.generation(),
671 acknowledged = !matches!(decline, ReplayBaselineDecline::NoAcknowledgedSnapshot),
672 "Did not advance the replay baseline"
673 ),
674 }
675 Ok(())
676 }
677
678 async fn reclaim_packages(&mut self) -> Result<(), SyncCycleFailure> {
688 use super::store::StorePackageReclaimCoverage;
689
690 let result = match self.authorization.reclaim_packages(self.settled).await {
691 Ok(result) => result,
692 Err(error) => return Err(SyncCycleFailure::operation("reclaim Store packages", error)),
693 };
694 let store = &result.store_packages;
695 let coverage = match &store.coverage {
696 StorePackageReclaimCoverage::Snapshot { generation } => {
697 format!("snapshot generation {generation}")
698 }
699 StorePackageReclaimCoverage::NoSnapshot => {
700 "no snapshot every active device has acknowledged".to_string()
701 }
702 StorePackageReclaimCoverage::MissingAcknowledgement { member, device_id } => {
703 format!("device {device_id} of member {member} has not acknowledged the snapshot")
704 }
705 StorePackageReclaimCoverage::NotOwner => {
706 "this device is not the current owner".to_string()
707 }
708 StorePackageReclaimCoverage::InputsUnchanged => {
709 "inputs unchanged since the last evaluation".to_string()
710 }
711 };
712 info!(
713 %coverage,
714 considered = store.targets_considered,
715 retained_for_replay = store.retained_for_replay,
716 retained_for_blob_reclaim = store.retained_for_blob_reclaim,
717 already_authorized = store.already_authorized,
718 authorized = store.authorized,
719 packages = result.packages_deleted,
720 copies = result.physical_copies_deleted,
721 stuck = result.stuck,
722 "Reclaimed snapshot-covered Store packages"
723 );
724 Ok(())
725 }
726}
727
728#[derive(Debug, thiserror::Error)]
729pub enum InitSyncError {
730 #[error("no synced tables configured; pass a non-empty synced-table set before sync starts")]
731 NoSyncedTables,
732 #[error("cloud cipher and blob path scheme describe different storage modes")]
733 IncoherentStorageRepresentation,
734 #[error("Store row routing initialization failed: {0}")]
735 RowRouting(coven_database::DbError),
736 #[error("Store initialization failed: {0}")]
737 Initialization(#[from] crate::sync::store::StoreInitializationError),
738 #[error("restoring the persisted pending rotation failed: {0}")]
739 PendingRotationRestore(#[source] coven_database::DbError),
740 #[error("prepared sync identity differs from its storage identity")]
741 StorageIdentityMismatch,
742 #[error("unlock requires an existing Store root")]
743 ExistingStoreRequired,
744}
745
746#[derive(Debug, Clone)]
749pub enum StoreInitialization {
750 CreateStore,
751 OpenStore {
752 expected_store_root: coven_protocol::store_commit::StoreRootRef,
753 },
754}
755
756pub(crate) trait CloudSyncCycleConnection:
761 CloudSyncObjectStorage + CloudSyncCipherStateAccess + CloudSyncRotationStateAccess
762{
763}
764
765impl CloudSyncCycleConnection for CloudSyncConnection {}
766
767pub struct PreparedSyncComponents {
770 database: coven_database::StoreDatabase,
771 store_dir: StoreDir,
772 local_blob_access: super::store::blob::LocalStoreBlobAccess,
773 storage: std::sync::Arc<CloudSyncConnection>,
774 identity: coven_keys::keys::UserKeypair,
775 initialization: StoreInitialization,
776 store_id: String,
777 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
778 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
779}
780
781impl PreparedSyncComponents {
782 pub async fn prepare(
783 database: coven_database::StoreDatabase,
784 store_dir: StoreDir,
785 storage: impl Into<std::sync::Arc<CloudSyncConnection>>,
786 identity: coven_keys::keys::UserKeypair,
787 initialization: StoreInitialization,
788 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
789 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
790 ) -> Result<Self, InitSyncError> {
791 #[cfg(any(test, feature = "test-utils"))]
792 database.assert_owns_payload_directory_for_test(&store_dir);
793 let storage = storage.into();
794 if !storage.uses_identity(&identity) {
795 return Err(InitSyncError::StorageIdentityMismatch);
796 }
797 if !database.has_synced_tables() {
802 return Err(InitSyncError::NoSyncedTables);
803 }
804 database
805 .validate_store_write_routing(routing_encryption.as_ref())
806 .map_err(InitSyncError::RowRouting)?;
807
808 let cipher_is_plaintext = storage.is_plaintext();
809 let representation_is_coherent = matches!(
810 (cipher_is_plaintext, storage.blob_path_scheme()),
811 (true, BlobPathScheme::Plain) | (false, BlobPathScheme::Hashed)
812 );
813 if !representation_is_coherent {
814 return Err(InitSyncError::IncoherentStorageRepresentation);
815 }
816
817 if !cipher_is_plaintext {
821 let gate = database
822 .load_rotation_gate()
823 .await
824 .map_err(InitSyncError::PendingRotationRestore)?;
825 storage.install_durable_gate(gate);
826 }
827
828 let store_id = storage.store_id().to_string();
829 let local_blob_access = super::store::blob::LocalStoreBlobAccess::new(
830 database.clone(),
831 store_dir.clone(),
832 super::store::blob::StoreBlobCache::new(database.clone(), store_dir.clone()),
833 );
834 Ok(Self {
835 database,
836 store_dir,
837 local_blob_access,
838 storage,
839 identity,
840 initialization,
841 store_id,
842 routing_encryption,
843 master_keys,
844 })
845 }
846
847 pub async fn initialize(
848 self,
849 observer: Option<std::sync::Arc<dyn BlobTransitionObserver>>,
850 ) -> Result<SyncComponents, InitSyncError> {
851 let storage: std::sync::Arc<dyn CloudSyncCycleConnection> = self.storage;
852 let store_storage: std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage> =
853 storage.clone();
854 let initialized = match self.initialization {
855 StoreInitialization::CreateStore => {
856 Store::create(
857 self.database.clone(),
858 store_storage.clone(),
859 self.store_dir.clone(),
860 &self.database.stamp(),
861 &self.identity,
862 )
863 .await
864 }
865 StoreInitialization::OpenStore {
866 expected_store_root,
867 } => {
868 Store::open(
869 self.database.clone(),
870 store_storage.clone(),
871 self.store_dir.clone(),
872 &expected_store_root,
873 &self.identity,
874 )
875 .await
876 }
877 }
878 .map_err(InitSyncError::Initialization)?;
879
880 let (store, device_id) = initialized.into_parts();
881 let blob_access = std::sync::Arc::new(super::store::blob::RemoteStoreBlobAccess::new(
882 self.local_blob_access.clone(),
883 super::store::blob::CurrentRemoteBlobSource::current(
884 self.database.clone(),
885 store_storage,
886 ),
887 ));
888 let blob_transitions = crate::blob::transition::ConnectedBlobTransitions::new(
889 crate::blob::transition::LocalBlobTransitions::new(
890 self.database.clone(),
891 self.store_dir.clone(),
892 ),
893 blob_access.clone(),
894 self.routing_encryption.clone(),
895 observer,
896 );
897 info!("Sync initialized (device: {})", device_id);
898 Ok(SyncComponents {
899 store: std::sync::Arc::new(store),
900 database: self.database,
901 local_blob_access: self.local_blob_access,
902 storage,
903 store_id: self.store_id,
904 device_id,
905 routing_encryption: self.routing_encryption,
906 master_keys: self.master_keys,
907 blob_transitions,
908 blob_access,
909 eager_fill_wanted: std::sync::Arc::default(),
910 settled: std::sync::Arc::default(),
911 })
912 }
913
914 pub async fn verify_open_store_key(&self) -> Result<(), InitSyncError> {
915 let StoreInitialization::OpenStore {
916 expected_store_root,
917 } = &self.initialization
918 else {
919 return Err(InitSyncError::ExistingStoreRequired);
920 };
921 super::store::protocol_root::verify_store_key_confirmation(
922 &self.database,
923 self.storage.as_ref(),
924 expected_store_root,
925 )
926 .await
927 .map_err(crate::sync::store::StoreInitializationError::from)
928 .map_err(InitSyncError::Initialization)
929 }
930}
931
932pub struct SyncComponents {
938 store: std::sync::Arc<Store>,
939 database: coven_database::StoreDatabase,
940 local_blob_access: super::store::blob::LocalStoreBlobAccess,
941 storage: std::sync::Arc<dyn CloudSyncCycleConnection>,
942 store_id: String,
946 device_id: String,
947 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
948 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
949 blob_transitions: crate::blob::transition::ConnectedBlobTransitions,
950 blob_access: std::sync::Arc<super::store::blob::RemoteStoreBlobAccess>,
951 eager_fill_wanted: std::sync::Arc<tokio::sync::Notify>,
956 settled: std::sync::Arc<super::store::SettledCycle>,
960}
961
962impl SyncComponents {
963 pub(crate) async fn fill_eager_cache(
964 &self,
965 cancel: tokio::sync::watch::Receiver<bool>,
966 status: &tokio::sync::watch::Sender<super::store::blob::eager_cache::EagerCacheFillStatus>,
967 ) -> Result<(), std::sync::Arc<super::store::blob::eager_cache::EagerCacheFillError>> {
968 super::store::blob::eager_cache::run(
969 &self.database,
970 self.blob_access.as_ref(),
971 cancel,
972 status,
973 )
974 .await
975 }
976
977 pub(crate) fn eager_fill_wanted(&self) -> &tokio::sync::Notify {
981 &self.eager_fill_wanted
982 }
983
984 pub(crate) async fn probe_storage(&self) -> Result<(), coven_protocol::objects::StorageError> {
985 self.storage.probe_provider().await
986 }
987
988 async fn pending_blocked_writes(
989 &self,
990 ) -> Result<Vec<coven_protocol::write::PendingWrite>, coven_database::DbError> {
991 Ok(self
992 .database
993 .pending_writes()
994 .await?
995 .into_iter()
996 .filter(|write| matches!(write.status, coven_protocol::write::WriteStatus::Blocked(_)))
997 .collect())
998 }
999
1000 pub(crate) async fn blocked_operations(
1005 &self,
1006 ) -> Result<Vec<super::sync_loop::BlockedOperation>, coven_database::DbError> {
1007 use super::sync_loop::BlockedOperation;
1008
1009 let mut blocked: Vec<BlockedOperation> = self
1010 .pending_blocked_writes()
1011 .await?
1012 .into_iter()
1013 .map(BlockedOperation::Write)
1014 .collect();
1015 blocked.extend(
1016 self.database
1017 .get_circle_operations()
1018 .await?
1019 .into_iter()
1020 .filter(|operation| {
1021 matches!(
1022 operation.state,
1023 coven_protocol::circle::CircleOperationState::Blocked { .. }
1024 )
1025 })
1026 .map(BlockedOperation::CircleOperation),
1027 );
1028 blocked.extend(
1029 self.database
1030 .stuck_reclaim_operations()
1031 .await?
1032 .into_iter()
1033 .map(BlockedOperation::Reclaim),
1034 );
1035 Ok(blocked)
1036 }
1037
1038 pub(crate) async fn retry_stuck_reclaim(
1041 &self,
1042 operation_id: coven_protocol::store_commit::ObjectHash,
1043 ) -> Result<(), coven_database::DbError> {
1044 self.database
1045 .retry_stuck_reclaim_operation(operation_id)
1046 .await
1047 }
1048
1049 pub(crate) async fn discard_blocked_write(
1050 &self,
1051 write_id: coven_protocol::write::WriteId,
1052 ) -> Result<Vec<coven_protocol::write::WriteId>, super::store::StoreError> {
1053 self.store
1054 .discard_blocked_write(write_id, self.routing_encryption.as_ref())
1055 .await
1056 }
1057
1058 pub(crate) async fn members(
1059 &self,
1060 ) -> Result<Vec<coven_protocol::membership::MemberInfo>, super::store::MembershipOpsError> {
1061 self.store.members().await
1062 }
1063
1064 pub(crate) async fn membership_conflict(
1065 &self,
1066 ) -> Result<Option<coven_protocol::MembershipConflictInfo>, super::store::MembershipOpsError>
1067 {
1068 self.store.membership_conflict().await
1069 }
1070
1071 pub(crate) async fn restore_membership(
1072 &self,
1073 ) -> Result<super::store::authorization::StoreRestoreMembership, super::store::MembershipOpsError>
1074 {
1075 self.store.restore_membership().await
1076 }
1077
1078 pub(crate) fn host_write_blob_staging(
1079 &self,
1080 runtime: tokio::runtime::Handle,
1081 ) -> super::store::HostWriteBlobStaging {
1082 self.store.host_write_blob_staging(runtime)
1083 }
1084
1085 pub(crate) async fn propose_device_exclusion(
1086 &self,
1087 device_id: coven_protocol::StoreDeviceId,
1088 ) -> Result<
1089 coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
1090 super::store::StoreDeviceExclusionError,
1091 > {
1092 self.store
1093 .propose_device_exclusion_for_device(device_id)
1094 .await
1095 }
1096
1097 pub(crate) async fn cancel_device_exclusion(
1098 &self,
1099 proposal: &coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
1100 ) -> Result<(), super::store::StoreDeviceExclusionError> {
1101 self.store.cancel_device_exclusion_proposal(proposal).await
1102 }
1103
1104 pub(crate) async fn finalize_device_exclusion(
1105 &self,
1106 proposal: &coven_protocol::store_commit::StoreDeviceExclusionProposalRef,
1107 ) -> Result<(), super::store::StoreDeviceExclusionError> {
1108 self.store
1109 .finalize_device_exclusion_proposal(proposal)
1110 .await
1111 }
1112
1113 pub(crate) async fn begin_owner_promotion(
1114 &self,
1115 device_id: coven_protocol::StoreDeviceId,
1116 ) -> Result<
1117 coven_protocol::store_commit::OwnerPromotionRequest,
1118 super::store::OwnerPromotionError,
1119 > {
1120 self.store.begin_owner_promotion_for_device(device_id).await
1121 }
1122
1123 pub(crate) async fn accept_owner_promotion(
1124 &self,
1125 request: coven_protocol::store_commit::OwnerPromotionRequest,
1126 ) -> Result<
1127 coven_protocol::store_commit::OwnerPromotionAcceptance,
1128 super::store::OwnerPromotionError,
1129 > {
1130 self.store.accept_owner_promotion(request).await
1131 }
1132
1133 pub(crate) async fn finalize_owner_promotion(
1134 &self,
1135 acceptance: coven_protocol::store_commit::OwnerPromotionAcceptance,
1136 ) -> Result<(), super::store::OwnerPromotionError> {
1137 let encryption = self
1138 .routing_encryption
1139 .as_ref()
1140 .ok_or(super::store::OwnerPromotionError::EncryptionRequired)?;
1141 self.store
1142 .finalize_owner_promotion(encryption, acceptance)
1143 .await
1144 .map(|_| ())
1145 }
1146
1147 pub(crate) async fn begin_device_join_bundle(
1148 &self,
1149 member_pubkey: &str,
1150 ) -> Result<crate::sync::DeviceJoinOfferBundle, super::store::DeviceJoinTransportError> {
1151 self.store.begin_device_join_bundle(member_pubkey).await
1152 }
1153
1154 pub(crate) async fn drive_device_join(
1155 &self,
1156 bundle: &crate::sync::DeviceJoinOfferBundle,
1157 policy: crate::sync::DeviceJoinApprovalPolicy<'_>,
1158 access_administrator: Option<&dyn crate::sync::DeviceProviderAccessAdministrator>,
1159 on_progress: &(dyn Fn(crate::sync::AdmittingDeviceJoinProgress) + Send + Sync),
1160 timing: crate::sync::DeviceJoinTransportTiming,
1161 ) -> Result<crate::sync::DeviceJoinDriveOutcome, super::store::DeviceJoinTransportError> {
1162 self.store
1163 .device_join_transport()
1164 .drive(bundle, policy, access_administrator, on_progress, timing)
1165 .await
1166 }
1167
1168 pub(crate) async fn abandon_device_join_transport(
1169 &self,
1170 bundle: &crate::sync::DeviceJoinOfferBundle,
1171 ) -> Result<crate::sync::DeviceJoinAbandonment, super::store::DeviceJoinTransportError> {
1172 self.store.device_join_transport().abandon(bundle).await
1173 }
1174
1175 pub(crate) async fn abort_device_join_transport(
1176 &self,
1177 bundle: &crate::sync::DeviceJoinOfferBundle,
1178 ) -> Result<(), super::store::DeviceJoinTransportError> {
1179 self.store.device_join_transport().abort(bundle).await
1180 }
1181
1182 pub(crate) async fn begin_device_join(
1183 &self,
1184 member_pubkey: &str,
1185 ) -> Result<crate::sync::DeviceJoinOffer, crate::sync::DeviceJoinError> {
1186 self.store.begin_device_join(member_pubkey).await
1187 }
1188
1189 pub(crate) async fn abandon_device_join(
1190 &self,
1191 offer: crate::sync::DeviceJoinOffer,
1192 ) -> Result<crate::sync::DeviceJoinAbandonment, crate::sync::DeviceJoinError> {
1193 self.store.abandon_device_join(offer).await
1194 }
1195
1196 pub(crate) async fn authorize_device_provider_access(
1197 &self,
1198 request: crate::sync::DeviceProviderAccessRequest,
1199 access_administrator: Option<&dyn crate::sync::DeviceProviderAccessAdministrator>,
1200 ) -> Result<crate::sync::DeviceProviderAdmissionApproval, crate::sync::DeviceJoinError> {
1201 self.store
1202 .authorize_device_provider_access(request, access_administrator)
1203 .await
1204 }
1205
1206 pub(crate) async fn accept_device_registration(
1207 &self,
1208 request: crate::sync::DeviceRegistrationRequest,
1209 ) -> Result<crate::sync::ProvisionalDeviceBootstrap, crate::sync::DeviceJoinError> {
1210 self.store.accept_device_registration_request(request).await
1211 }
1212
1213 pub(crate) async fn publish_device_provider_challenge(
1214 &self,
1215 bootstrap: crate::sync::ProvisionalDeviceBootstrap,
1216 ) -> Result<crate::sync::ProviderReadyDeviceBootstrap, crate::sync::DeviceJoinError> {
1217 self.store
1218 .publish_device_provider_challenge(bootstrap)
1219 .await
1220 }
1221
1222 pub(crate) async fn complete_device_provider_admission(
1223 &self,
1224 readiness: crate::sync::DeviceJoinReadiness,
1225 ) -> Result<crate::sync::DeviceProviderAdmissionCompletion, crate::sync::DeviceJoinError> {
1226 self.store
1227 .complete_device_provider_admission(readiness)
1228 .await
1229 }
1230
1231 pub(crate) async fn finalize_device_join(
1232 &self,
1233 completion: crate::sync::DeviceProviderAdmissionCompletion,
1234 ) -> Result<crate::sync::DeviceJoinActivation, crate::sync::DeviceJoinError> {
1235 self.store.finalize_device_join(completion).await
1236 }
1237
1238 pub(crate) fn blob_path_scheme(&self) -> BlobPathScheme {
1239 self.store.blob_path_scheme()
1240 }
1241
1242 pub(crate) fn is_encrypted(&self) -> bool {
1243 !self.storage.is_plaintext()
1244 }
1245
1246 pub(crate) async fn drain_uploads(
1247 &self,
1248 clock: &dyn coven_foundation::clock::Clock,
1249 observer: Option<&dyn BlobTransitionObserver>,
1250 ) -> Result<crate::blob::DrainOutcome, crate::sync::store::StoreError> {
1251 self.store
1252 .authorize_writer()
1253 .await
1254 .map_err(crate::sync::store::StoreError::from)?
1255 .drain_uploads(clock, self.routing_encryption.as_ref(), observer)
1256 .await
1257 .map_err(crate::sync::store::StoreError::from)
1258 }
1259
1260 pub(crate) async fn make_remote(
1261 &self,
1262 root_table: &str,
1263 root_id: &str,
1264 root_label: &str,
1265 pin: bool,
1266 refs: Vec<coven_protocol::blob::RowBlobRef>,
1267 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
1268 self.blob_transitions
1269 .make_remote(root_table, root_id, root_label, pin, refs)
1270 .await
1271 }
1272
1273 pub(crate) async fn make_remote_batch(
1274 &self,
1275 root_table: &str,
1276 roots: Vec<crate::blob::MakeRemoteRoot>,
1277 pin: bool,
1278 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
1279 self.blob_transitions
1280 .make_remote_batch(root_table, roots, pin)
1281 .await
1282 }
1283
1284 pub(crate) async fn cancel_make_remote(
1285 &self,
1286 root_table: &str,
1287 root_id: &str,
1288 ) -> Result<(), crate::blob::transition::MakeRemoteError> {
1289 self.blob_transitions
1290 .cancel_make_remote(root_table, root_id)
1291 .await
1292 }
1293
1294 pub(crate) async fn make_local(
1295 &self,
1296 root_table: &str,
1297 root_id: &str,
1298 dest: &std::collections::HashMap<String, std::path::PathBuf>,
1299 cancel: &tokio::sync::watch::Receiver<bool>,
1300 ) -> Result<(), crate::blob::transition::MakeLocalError> {
1301 self.blob_transitions
1302 .make_local(root_table, root_id, dest, cancel)
1303 .await
1304 }
1305
1306 pub(crate) async fn admit_member(
1307 &self,
1308 public_key_hex: &str,
1309 member_email: Option<&str>,
1310 role: coven_protocol::membership::MemberRole,
1311 store_name: &str,
1312 ) -> Result<crate::sync::store::MemberAdmission, super::store::MembershipOpsError> {
1313 let encryption = self
1314 .routing_encryption
1315 .as_ref()
1316 .ok_or(super::store::MembershipOpsError::NotEncryptedHome)?;
1317 self.store
1318 .admit_member(
1319 public_key_hex,
1320 member_email,
1321 role,
1322 encryption,
1323 &self.store_id,
1324 store_name,
1325 )
1326 .await
1327 }
1328
1329 pub(crate) async fn remove_member(
1330 &self,
1331 public_key_hex: &str,
1332 ) -> Result<String, super::store::MembershipOpsError> {
1333 let encryption = self
1334 .routing_encryption
1335 .as_ref()
1336 .ok_or(super::store::MembershipOpsError::NotEncryptedHome)?;
1337 self.store
1338 .remove_member(
1339 public_key_hex,
1340 encryption,
1341 self.master_keys.as_ref(),
1342 self.storage.as_ref(),
1343 self.storage.as_ref(),
1344 )
1345 .await
1346 }
1347
1348 pub(crate) async fn resolve_membership_conflict(
1349 &self,
1350 choice: &coven_protocol::membership::MembershipConflictChoice,
1351 ) -> Result<(), super::store::MembershipOpsError> {
1352 self.store
1353 .resolve_membership_conflict(choice, &self.database.stamp())
1354 .await?;
1355 Ok(())
1356 }
1357
1358 pub(crate) async fn create_circle(
1359 &self,
1360 name: &str,
1361 ) -> Result<coven_protocol::circle::CircleId, super::store::CircleOperationError> {
1362 self.store
1363 .circles()
1364 .create_circle(&self.database.stamp(), name)
1365 .await
1366 }
1367
1368 pub(crate) async fn rename_circle(
1369 &self,
1370 circle_id: coven_protocol::circle::CircleId,
1371 name: &str,
1372 ) -> Result<(), super::store::CircleOperationError> {
1373 self.store
1374 .circles()
1375 .rename_circle(&self.database.stamp(), circle_id, name)
1376 .await
1377 }
1378
1379 pub(crate) async fn resolve_circle_control(
1380 &self,
1381 circle_id: coven_protocol::circle::CircleId,
1382 chosen: coven_protocol::circle::CircleControlCoord,
1383 ) -> Result<(), super::store::CircleOperationError> {
1384 self.store
1385 .circles()
1386 .resolve_circle_control(circle_id, chosen)
1387 .await
1388 }
1389
1390 pub(crate) async fn delete_circle(
1391 &self,
1392 circle_id: coven_protocol::circle::CircleId,
1393 ) -> Result<(), super::store::CircleOperationError> {
1394 self.store.circles().delete_circle(circle_id).await
1395 }
1396
1397 pub(crate) async fn add_circle_member(
1398 &self,
1399 circle_id: coven_protocol::circle::CircleId,
1400 member_pubkey: String,
1401 role: coven_protocol::circle::CircleRole,
1402 ) -> Result<(), super::store::CircleOperationError> {
1403 use super::store::CircleOperationError;
1404 let routing_encryption = self
1408 .routing_encryption
1409 .as_ref()
1410 .ok_or(CircleOperationError::BrowsableStorage)?;
1411 let mut authorization = self
1412 .store
1413 .authorize_writer()
1414 .await
1415 .map_err(CircleOperationError::from)?;
1416 authorization
1417 .publish_pending_store_writes(Some(routing_encryption))
1418 .await
1419 .map_err(CircleOperationError::from)?;
1420 let bootstrap = authorization
1421 .circles()
1422 .snapshots()
1423 .capture_circle_snapshot_cut(routing_encryption, circle_id)
1424 .await?;
1425 let routing_key = coven_protocol::circle::derive_row_routing_key(
1426 routing_encryption,
1427 self.store.store_root().store_root_hash,
1428 )
1429 .map_err(CircleOperationError::from)?;
1430 authorization
1431 .circles()
1432 .add_circle_member(circle_id, member_pubkey, role, bootstrap, &routing_key)
1433 .await
1434 }
1435
1436 pub(crate) async fn remove_circle_member(
1437 &self,
1438 circle_id: coven_protocol::circle::CircleId,
1439 member_pubkey: String,
1440 ) -> Result<coven_protocol::circle::CircleOperationId, super::store::CircleOperationError> {
1441 self.store
1442 .circles()
1443 .remove_circle_member(circle_id, member_pubkey)
1444 .await
1445 }
1446
1447 pub(crate) async fn cancel_circle_epoch_close(
1448 &self,
1449 circle_id: coven_protocol::circle::CircleId,
1450 ) -> Result<coven_protocol::circle::CircleOperationId, super::store::CircleOperationError> {
1451 self.store
1452 .circles()
1453 .cancel_circle_epoch_close(circle_id)
1454 .await
1455 }
1456
1457 pub(crate) async fn exclude_circle_close_device(
1458 &self,
1459 circle_id: coven_protocol::circle::CircleId,
1460 excluded_device_id: coven_protocol::store_commit::StoreDeviceId,
1461 ) -> Result<(), super::store::CircleOperationError> {
1462 self.store
1463 .circles()
1464 .exclude_circle_close_device(circle_id, excluded_device_id)
1465 .await
1466 }
1467
1468 pub(crate) async fn retry_circle_operation(
1469 &self,
1470 operation_id: &coven_protocol::circle::CircleOperationId,
1471 ) -> Result<(), super::store::CircleOperationError> {
1472 self.store
1473 .circles()
1474 .retry_circle_operation(operation_id, self.routing_encryption.as_ref())
1475 .await
1476 }
1477
1478 pub(crate) async fn discard_circle_operation(
1479 &self,
1480 operation_id: &coven_protocol::circle::CircleOperationId,
1481 ) -> Result<(), super::store::CircleOperationError> {
1482 self.store
1483 .circles()
1484 .discard_circle_operation(operation_id)
1485 .await
1486 }
1487
1488 pub(crate) async fn circle_close_status(
1489 &self,
1490 circle_id: coven_protocol::circle::CircleId,
1491 ) -> Result<coven_protocol::circle::CircleCloseStatus, super::store::CircleOperationError> {
1492 self.store.circles().circle_close_status(circle_id).await
1493 }
1494
1495 pub(crate) fn provider_requests(
1498 &self,
1499 ) -> Option<std::sync::Arc<dyn coven_foundation::stage_timing::ProviderRequests>> {
1500 self.storage.provider_requests()
1501 }
1502
1503 pub async fn run_cycle(
1504 &self,
1505 clock: &dyn coven_foundation::clock::Clock,
1506 observer: Option<&dyn BlobTransitionObserver>,
1507 ) -> Result<SyncCycleResult, SyncCycleFailure> {
1508 let authorization =
1509 self.store.authorize_writer().await.map_err(|error| {
1510 SyncCycleFailure::operation("authorize local Store writer", error)
1511 })?;
1512 AuthorizedSyncCycle {
1513 device_id: &self.device_id,
1514 clock,
1515 cipher: self.storage.as_ref(),
1516 pending_rotation: self.storage.as_ref(),
1517 master_keys: Some(self.master_keys.as_ref()),
1518 routing_encryption: self.routing_encryption.as_ref(),
1519 local_blob_access: &self.local_blob_access,
1520 observer,
1521 settled: self.settled.as_ref(),
1522 authorization,
1523 }
1524 .run()
1525 .await
1526 .inspect(|result| {
1527 if result.changesets_applied > 0 {
1528 self.eager_fill_wanted.notify_one();
1529 }
1530 })
1531 }
1532
1533 #[cfg(any(test, feature = "test-utils"))]
1534 #[allow(clippy::too_many_arguments)]
1535 pub(crate) fn from_retained_test_device<S>(
1536 store: std::sync::Arc<Store>,
1537 database: coven_database::StoreDatabase,
1538 store_dir: StoreDir,
1539 storage: std::sync::Arc<S>,
1540 store_id: String,
1541 device_id: String,
1542 master_keys: std::sync::Arc<dyn coven_keys::keys::MasterKeyCustody>,
1543 settled: std::sync::Arc<super::store::SettledCycle>,
1548 ) -> Self
1549 where
1550 S: CloudSyncCycleConnection + 'static,
1551 {
1552 database.assert_owns_payload_directory_for_test(&store_dir);
1553 let storage: std::sync::Arc<dyn CloudSyncCycleConnection> = storage;
1554 let store_storage: std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage> =
1555 storage.clone();
1556 let local_blob_access = super::store::blob::LocalStoreBlobAccess::new(
1557 database.clone(),
1558 store_dir.clone(),
1559 super::store::blob::StoreBlobCache::new(database.clone(), store_dir.clone()),
1560 );
1561 let blob_access = std::sync::Arc::new(super::store::blob::RemoteStoreBlobAccess::new(
1562 local_blob_access.clone(),
1563 super::store::blob::CurrentRemoteBlobSource::current(database.clone(), store_storage),
1564 ));
1565 let blob_transitions = crate::blob::transition::ConnectedBlobTransitions::new(
1566 crate::blob::transition::LocalBlobTransitions::new(database.clone(), store_dir.clone()),
1567 blob_access.clone(),
1568 None,
1569 None,
1570 );
1571 Self {
1572 store,
1573 database,
1574 local_blob_access,
1575 store_id,
1576 storage,
1577 device_id,
1578 routing_encryption: None,
1579 master_keys,
1580 blob_transitions,
1581 blob_access,
1582 eager_fill_wanted: std::sync::Arc::default(),
1583 settled,
1584 }
1585 }
1586
1587 #[cfg(any(test, feature = "test-utils"))]
1588 pub async fn list_storage_objects_for_test(
1589 &self,
1590 prefix: &str,
1591 ) -> Result<Vec<String>, coven_protocol::objects::StorageError> {
1592 self.storage.list_provider_keys_for_test(prefix).await
1593 }
1594
1595 #[cfg(any(test, feature = "test-utils"))]
1596 pub fn uses_storage_for_test(
1597 &self,
1598 expected: &std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage>,
1599 ) -> bool {
1600 let actual: std::sync::Arc<dyn coven_storage::CloudSyncObjectStorage> =
1601 self.storage.clone();
1602 std::sync::Arc::ptr_eq(&actual, expected)
1603 }
1604
1605 #[cfg(any(test, feature = "test-utils"))]
1606 pub fn uses_store_dir_for_test(&self, expected: &StoreDir) -> bool {
1607 self.local_blob_access.uses_store_dir_for_test(expected)
1608 }
1609
1610 #[cfg(any(test, feature = "test-utils"))]
1611 pub fn encryption_generation_for_test(&self) -> Option<u64> {
1612 self.storage.current_generation()
1613 }
1614
1615 #[cfg(any(test, feature = "test-utils"))]
1616 pub fn open_sealed_blob_for_test(
1617 &self,
1618 stored: &[u8],
1619 aad_context: &[u8],
1620 ) -> Result<
1621 (coven_keys::encryption::KeyFingerprint, Vec<u8>),
1622 coven_keys::encryption::EncryptionError,
1623 > {
1624 self.storage.open_sealed_blob_for_test(stored, aad_context)
1625 }
1626
1627 #[cfg(any(test, feature = "test-utils"))]
1628 pub fn adopt_key_rotation(
1629 &self,
1630 encryption: coven_keys::encryption::EncryptionService,
1631 ) -> Result<String, coven_keys::keys::KeyError> {
1632 CloudSyncCipherStateAccess::adopt_key_rotation(
1633 self.storage.as_ref(),
1634 &encryption,
1635 self.master_keys.as_ref(),
1636 )
1637 .map(|adopted| adopted.fingerprint().to_string())
1638 }
1639}