1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4#[serde(rename_all = "snake_case", deny_unknown_fields)]
5pub enum ActiveStorePublicationOwner {
6 StoreWrite(WriteId),
7 StoreAcknowledgement,
8 DeviceJoin(coven_protocol::store_commit::DeviceJoinAttemptId),
9 DeviceExclusion(ObjectHash),
10 MembershipMutation,
11 OwnerPromotion(coven_protocol::store_commit::OwnerPromotionId),
12 Reclaim(ObjectHash),
13 CircleOperation(coven_protocol::circle::CircleOperationId),
14 OwnerRecovery,
15 Snapshot,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(rename_all = "snake_case", deny_unknown_fields)]
20pub enum ActiveStorePublicationAttempt {
21 AwaitingPreparation {
22 write_id: WriteId,
23 author_registration: StoreDeviceRegistrationRef,
24 coord: StoreCommitCoord,
25 },
26 Discarding {
27 write_id: WriteId,
28 author_registration: StoreDeviceRegistrationRef,
29 coord: StoreCommitCoord,
30 },
31 Commit {
32 write_id: WriteId,
33 author_registration: StoreDeviceRegistrationRef,
34 coord: StoreCommitCoord,
35 publication: coven_protocol::prepared_commit::PreparedStorePublication,
36 },
37 MembershipAbandonment {
38 candidate: Box<coven_protocol::prepared_commit::PreparedStoreOperationCommit>,
39 },
40 CompletingCoveredWrite {
41 write_id: WriteId,
42 position: coven_protocol::write::SnapshotCoveredPosition,
43 publication: coven_protocol::prepared_commit::PreparedStorePublication,
44 },
45 Snapshot {
46 publication: coven_protocol::prepared_commit::PreparedStorePublication,
47 retired_objects: Vec<coven_protocol::objects::ExactObjectRef>,
48 },
49 SnapshotSuperseded {
50 publication: coven_protocol::prepared_commit::PreparedStorePublication,
51 snapshot: PublishedStoreSnapshot,
52 retired_objects: Vec<coven_protocol::objects::ExactObjectRef>,
53 },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct RetiredStoreCandidate {
59 pub nonactivation: coven_protocol::remote_object::CandidateNonactivation,
60 pub inputs: RetiredStoreCandidateInputs,
61 pub publications: Vec<coven_protocol::store_commit::StorePublicationRef>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
65#[serde(rename_all = "snake_case", deny_unknown_fields)]
66pub enum RetiredStoreCandidateInputs {
67 Write(Vec<PreparedAudienceBlob>),
68 Acknowledgement(coven_protocol::store_commit::RetainedVerifiedActivatedAck),
69 Membership(coven_protocol::membership_mutation::PreparedMembershipPublication),
70}
71
72impl RetiredStoreCandidate {
73 pub(crate) fn blobs(&self) -> &[PreparedAudienceBlob] {
74 match &self.inputs {
75 RetiredStoreCandidateInputs::Write(blobs) => blobs,
76 RetiredStoreCandidateInputs::Acknowledgement(_)
77 | RetiredStoreCandidateInputs::Membership(_) => &[],
78 }
79 }
80
81 pub(crate) fn candidate(&self) -> Result<StoreBatchCommitRef, DbError> {
82 self.nonactivation.reference().map_err(DbError::from)
83 }
84
85 pub(crate) fn objects(&self) -> Result<Vec<coven_protocol::objects::ExactObjectRef>, DbError> {
86 self.nonactivation.validate()?;
87 let commit: coven_protocol::store_commit::StoreBatchCommit =
88 serde_json::from_slice(&self.nonactivation.candidate().canonical_signed_bytes)
89 .map_err(|error| DbError::context("retired Store write candidate", error))?;
90 let mut objects = crate::candidate_graph_exact_objects(&commit)?
91 .into_iter()
92 .collect::<std::collections::BTreeSet<_>>();
93 objects.insert(self.nonactivation.candidate().object.clone());
94 objects.extend(self.blobs().iter().map(|blob| blob.blob().object().clone()));
95 if let RetiredStoreCandidateInputs::Membership(publication) = &self.inputs {
96 objects.extend(publication.candidate_object_refs(&commit, &self.candidate()?)?);
97 }
98 if let RetiredStoreCandidateInputs::Acknowledgement(proof) = &self.inputs {
99 proof.validate_predecessors()?;
100 if commit.acknowledgement() != Some(&proof.acknowledgement.0)
101 || proof.activating_commit != self.candidate()?
102 {
103 return Err(DbError::Message(
104 "retired acknowledgement proof names another candidate".into(),
105 ));
106 }
107 objects.extend(commit.retained_operation_objects()?);
108 objects.extend(
109 proof
110 .predecessors
111 .iter()
112 .map(|(reference, _)| reference.object.clone()),
113 );
114 }
115 Ok(objects.into_iter().collect())
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct ActiveStorePublication {
124 owner: ActiveStorePublicationOwner,
125 attempt: ActiveStorePublicationAttempt,
126 superseded_entry: Option<coven_protocol::store_commit::StorePublicationRef>,
130 retired_candidates: Vec<RetiredStoreCandidate>,
131}
132
133impl ActiveStorePublication {
134 pub fn for_commit(
135 owner: ActiveStorePublicationOwner,
136 candidate: &coven_protocol::prepared_commit::PreparedStoreOperationCommit,
137 ) -> Result<Self, DbError> {
138 Self::commit(
139 owner,
140 candidate.commit.write_id.clone(),
141 candidate.commit.author_registration.clone(),
142 candidate.reference.coord.clone(),
143 candidate.publication.clone(),
144 )
145 }
146
147 pub fn commit(
148 owner: ActiveStorePublicationOwner,
149 write_id: WriteId,
150 author_registration: StoreDeviceRegistrationRef,
151 coord: StoreCommitCoord,
152 publication: coven_protocol::prepared_commit::PreparedStorePublication,
153 ) -> Result<Self, DbError> {
154 if matches!(&owner, ActiveStorePublicationOwner::StoreWrite(owner) if owner != &write_id) {
155 return Err(DbError::Message(
156 "active Store-write owner differs from its logical write".to_string(),
157 ));
158 }
159 let attempt = ActiveStorePublicationAttempt::Commit {
160 write_id,
161 author_registration,
162 coord,
163 publication,
164 };
165 Self::validated(owner, attempt)
166 }
167
168 pub fn snapshot(
169 publication: coven_protocol::prepared_commit::PreparedStorePublication,
170 ) -> Result<Self, DbError> {
171 let owner = ActiveStorePublicationOwner::Snapshot;
172 let attempt = ActiveStorePublicationAttempt::Snapshot {
173 publication,
174 retired_objects: Vec::new(),
175 };
176 Self::validated(owner, attempt)
177 }
178
179 fn validated(
180 owner: ActiveStorePublicationOwner,
181 attempt: ActiveStorePublicationAttempt,
182 ) -> Result<Self, DbError> {
183 let publication = match &attempt {
184 ActiveStorePublicationAttempt::AwaitingPreparation { .. }
185 | ActiveStorePublicationAttempt::Discarding { .. }
186 | ActiveStorePublicationAttempt::CompletingCoveredWrite { .. }
187 | ActiveStorePublicationAttempt::SnapshotSuperseded { .. } => {
188 return Err(DbError::Message(
189 "an awaiting reservation has no prepared publication".to_string(),
190 ));
191 }
192 ActiveStorePublicationAttempt::Commit { publication, .. }
193 | ActiveStorePublicationAttempt::Snapshot { publication, .. } => publication,
194 ActiveStorePublicationAttempt::MembershipAbandonment { candidate } => {
195 &candidate.publication
196 }
197 };
198 let payload_matches = match (&attempt, &publication.entry.payload) {
199 (
200 ActiveStorePublicationAttempt::Commit {
201 author_registration,
202 coord,
203 ..
204 },
205 coven_protocol::store_commit::StorePublicationPayload::Commit(reference),
206 ) => {
207 &publication.entry.author_registration == author_registration
208 && &reference.coord == coord
209 }
210 (
211 ActiveStorePublicationAttempt::Snapshot { .. },
212 coven_protocol::store_commit::StorePublicationPayload::Snapshot(_),
213 ) => true,
214 (
215 ActiveStorePublicationAttempt::MembershipAbandonment { candidate },
216 coven_protocol::store_commit::StorePublicationPayload::Commit(reference),
217 ) => {
218 owner == ActiveStorePublicationOwner::MembershipMutation
219 && reference == &candidate.reference
220 && publication.entry.author_registration == candidate.commit.author_registration
221 && !candidate.commit.abandoned_candidates().is_empty()
222 }
223 _ => false,
224 };
225 if !payload_matches {
226 return Err(DbError::Message(
227 "active Store publication source differs from its exact attempt".to_string(),
228 ));
229 }
230 publication
231 .reference()
232 .map_err(|error| DbError::context("active Store publication attempt", error))?;
233 Ok(Self {
234 owner,
235 attempt,
236 superseded_entry: None,
237 retired_candidates: Vec::new(),
238 })
239 }
240
241 pub fn owner(&self) -> &ActiveStorePublicationOwner {
242 &self.owner
243 }
244
245 pub(crate) fn author_registration(&self) -> &StoreDeviceRegistrationRef {
246 match &self.attempt {
247 ActiveStorePublicationAttempt::AwaitingPreparation {
248 author_registration,
249 ..
250 }
251 | ActiveStorePublicationAttempt::Discarding {
252 author_registration,
253 ..
254 }
255 | ActiveStorePublicationAttempt::Commit {
256 author_registration,
257 ..
258 } => author_registration,
259 ActiveStorePublicationAttempt::MembershipAbandonment { candidate } => {
260 &candidate.commit.author_registration
261 }
262 ActiveStorePublicationAttempt::CompletingCoveredWrite { position, .. } => {
263 &position.author_registration
264 }
265 ActiveStorePublicationAttempt::Snapshot { publication, .. }
266 | ActiveStorePublicationAttempt::SnapshotSuperseded { publication, .. } => {
267 &publication.entry.author_registration
268 }
269 }
270 }
271
272 pub fn attempt(
273 &self,
274 ) -> Result<&coven_protocol::prepared_commit::PreparedStorePublication, DbError> {
275 match &self.attempt {
276 ActiveStorePublicationAttempt::AwaitingPreparation { .. } => Err(DbError::Message(
277 "reserved Store write is awaiting candidate preparation".to_string(),
278 )),
279 ActiveStorePublicationAttempt::Discarding { .. } => Err(DbError::Message(
280 "reserved Store write is being discarded".to_string(),
281 )),
282 ActiveStorePublicationAttempt::Commit { publication, .. }
283 | ActiveStorePublicationAttempt::Snapshot { publication, .. }
284 | ActiveStorePublicationAttempt::SnapshotSuperseded { publication, .. }
285 | ActiveStorePublicationAttempt::CompletingCoveredWrite { publication, .. } => {
286 Ok(publication)
287 }
288 ActiveStorePublicationAttempt::MembershipAbandonment { candidate } => {
289 Ok(&candidate.publication)
290 }
291 }
292 }
293
294 pub fn covered_write_position(
295 &self,
296 ) -> Option<&coven_protocol::write::SnapshotCoveredPosition> {
297 match &self.attempt {
298 ActiveStorePublicationAttempt::CompletingCoveredWrite { position, .. } => {
299 Some(position)
300 }
301 _ => None,
302 }
303 }
304
305 pub(crate) fn begin_covered_write_completion(
306 &self,
307 position: coven_protocol::write::SnapshotCoveredPosition,
308 ) -> Result<Self, DbError> {
309 let ActiveStorePublicationAttempt::Commit {
310 write_id,
311 author_registration,
312 coord,
313 publication,
314 } = &self.attempt
315 else {
316 return Err(DbError::Message(
317 "covered completion requires a prepared Store write".into(),
318 ));
319 };
320 if self.owner != ActiveStorePublicationOwner::StoreWrite(write_id.clone())
321 || position.author_registration != *author_registration
322 || position.coord != *coord
323 || !self.retired_candidates.is_empty()
324 {
325 return Err(DbError::Message(
326 "covered completion differs from its reserved write or retains candidate cleanup"
327 .into(),
328 ));
329 }
330 let mut completed = self.clone();
331 completed.attempt = ActiveStorePublicationAttempt::CompletingCoveredWrite {
332 write_id: write_id.clone(),
333 position,
334 publication: publication.clone(),
335 };
336 Ok(completed)
337 }
338
339 pub fn is_awaiting_preparation(&self) -> bool {
340 matches!(
341 self.attempt,
342 ActiveStorePublicationAttempt::AwaitingPreparation { .. }
343 )
344 }
345
346 pub fn is_discarding(&self) -> bool {
347 matches!(
348 self.attempt,
349 ActiveStorePublicationAttempt::Discarding { .. }
350 )
351 }
352
353 pub(crate) fn begin_discard(&self) -> Result<Self, DbError> {
354 let ActiveStorePublicationAttempt::AwaitingPreparation {
355 write_id,
356 author_registration,
357 coord,
358 } = &self.attempt
359 else {
360 return Err(DbError::Message(
361 "discard requires a write whose old candidate has been retired".to_string(),
362 ));
363 };
364 if self.owner != ActiveStorePublicationOwner::StoreWrite(write_id.clone()) {
365 return Err(DbError::Message(
366 "discard reservation differs from its Store-write owner".to_string(),
367 ));
368 }
369 let mut discarded = self.clone();
370 discarded.attempt = ActiveStorePublicationAttempt::Discarding {
371 write_id: write_id.clone(),
372 author_registration: author_registration.clone(),
373 coord: coord.clone(),
374 };
375 Ok(discarded)
376 }
377
378 pub fn retired_candidates(&self) -> &[RetiredStoreCandidate] {
379 &self.retired_candidates
380 }
381
382 pub fn retired_snapshot_objects(&self) -> &[coven_protocol::objects::ExactObjectRef] {
383 match &self.attempt {
384 ActiveStorePublicationAttempt::Snapshot {
385 retired_objects, ..
386 }
387 | ActiveStorePublicationAttempt::SnapshotSuperseded {
388 retired_objects, ..
389 } => retired_objects,
390 _ => &[],
391 }
392 }
393
394 pub(crate) fn retain_snapshot_cleanup(
395 &mut self,
396 objects: Vec<coven_protocol::objects::ExactObjectRef>,
397 ) -> Result<(), DbError> {
398 let ActiveStorePublicationAttempt::Snapshot {
399 retired_objects, ..
400 } = &mut self.attempt
401 else {
402 return Err(DbError::Message(
403 "snapshot cleanup has another publication owner".into(),
404 ));
405 };
406 if !retired_objects.is_empty() {
407 return Err(DbError::Message(
408 "snapshot cleanup is already pending".into(),
409 ));
410 }
411 *retired_objects = objects;
412 Ok(())
413 }
414
415 pub(crate) fn complete_snapshot_cleanup(&mut self) -> Result<(), DbError> {
416 let retired_objects = match &mut self.attempt {
417 ActiveStorePublicationAttempt::Snapshot {
418 retired_objects, ..
419 }
420 | ActiveStorePublicationAttempt::SnapshotSuperseded {
421 retired_objects, ..
422 } => retired_objects,
423 _ => {
424 return Err(DbError::Message(
425 "snapshot cleanup has another publication owner".into(),
426 ));
427 }
428 };
429 if retired_objects.is_empty() {
430 return Err(DbError::Message("snapshot has no pending cleanup".into()));
431 }
432 retired_objects.clear();
433 Ok(())
434 }
435
436 pub fn superseding_snapshot(&self) -> Option<&PublishedStoreSnapshot> {
437 match &self.attempt {
438 ActiveStorePublicationAttempt::SnapshotSuperseded { snapshot, .. } => Some(snapshot),
439 _ => None,
440 }
441 }
442
443 pub(crate) fn supersede_snapshot(
444 &self,
445 snapshot: PublishedStoreSnapshot,
446 retired_objects: Vec<coven_protocol::objects::ExactObjectRef>,
447 ) -> Result<Self, DbError> {
448 let ActiveStorePublicationAttempt::Snapshot {
449 publication,
450 retired_objects: pending,
451 } = &self.attempt
452 else {
453 return Err(DbError::Message(
454 "only a pending snapshot request can be superseded".into(),
455 ));
456 };
457 if !pending.is_empty() || self.owner != ActiveStorePublicationOwner::Snapshot {
458 return Err(DbError::Message(
459 "snapshot supersession has unfinished candidate cleanup".into(),
460 ));
461 }
462 let mut superseded = self.clone();
463 superseded.attempt = ActiveStorePublicationAttempt::SnapshotSuperseded {
464 publication: publication.clone(),
465 snapshot,
466 retired_objects,
467 };
468 Ok(superseded)
469 }
470
471 pub(crate) fn await_preparation(
472 &self,
473 cleanup: RetiredStoreCandidate,
474 ) -> Result<Self, DbError> {
475 if self.covered_write_position().is_some() {
476 return Err(DbError::Message(
477 "covered write completion cannot return to preparation".into(),
478 ));
479 }
480 let Some((write_id, author_registration, coord)) = self.commit_reservation() else {
481 return Err(DbError::Message(
482 "snapshot publication cannot reserve a Store write".to_string(),
483 ));
484 };
485 let candidate = cleanup.candidate()?;
486 let owner_matches = match (&self.owner, &cleanup.inputs) {
487 (
488 ActiveStorePublicationOwner::StoreWrite(owner),
489 RetiredStoreCandidateInputs::Write(_),
490 ) => owner == write_id,
491 (
492 ActiveStorePublicationOwner::MembershipMutation,
493 RetiredStoreCandidateInputs::Membership(_),
494 ) => matches!(
495 cleanup.nonactivation.proof(),
496 coven_protocol::remote_object::CandidateNonactivationProof::AuthorityRetirement { .. }
497 ) && self.retired_candidates.is_empty(),
498 _ => false,
499 };
500 if !owner_matches
501 || candidate.coord != *coord
502 || self.attempt()?.entry.payload
503 != coven_protocol::store_commit::StorePublicationPayload::Commit(candidate.clone())
504 {
505 return Err(DbError::Message(
506 "replacement cleanup differs from the reserved Store write".to_string(),
507 ));
508 }
509 cleanup.objects()?;
510 let mut retired_candidates = self.retired_candidates.clone();
511 retired_candidates.push(cleanup);
512 Ok(Self {
513 owner: self.owner.clone(),
514 attempt: ActiveStorePublicationAttempt::AwaitingPreparation {
515 write_id: write_id.clone(),
516 author_registration: author_registration.clone(),
517 coord: coord.clone(),
518 },
519 superseded_entry: None,
520 retired_candidates,
521 })
522 }
523
524 pub(crate) fn complete_retired_candidate_cleanup(&mut self) -> Result<(), DbError> {
525 if self.retired_candidates.is_empty() {
526 return Err(DbError::Message(
527 "reserved write has no retired candidate cleanup".to_string(),
528 ));
529 }
530 self.retired_candidates.clear();
531 Ok(())
532 }
533
534 pub(crate) fn continue_membership_after_abandonment(
535 &self,
536 cleanup: RetiredStoreCandidate,
537 ) -> Result<Self, DbError> {
538 let Some((write_id, author_registration, coord)) = self.commit_reservation() else {
539 return Err(DbError::Message(
540 "membership abandonment has no reserved author position".into(),
541 ));
542 };
543 if self.owner != ActiveStorePublicationOwner::MembershipMutation
544 || !matches!(
545 self.attempt,
546 ActiveStorePublicationAttempt::MembershipAbandonment { .. }
547 )
548 || cleanup.candidate()?.coord != *coord
549 || !matches!(cleanup.inputs, RetiredStoreCandidateInputs::Membership(_))
550 || !self.retired_candidates.is_empty()
551 || self.superseded_entry.is_some()
552 {
553 return Err(DbError::Message(
554 "membership abandonment differs from its reserved mutation".into(),
555 ));
556 }
557 cleanup.objects()?;
558 let sequence = coord.sequence.checked_add(1).ok_or_else(|| {
559 DbError::Message("membership continuation exhausts its author sequence".into())
560 })?;
561 Ok(Self {
562 owner: self.owner.clone(),
563 attempt: ActiveStorePublicationAttempt::AwaitingPreparation {
564 write_id: write_id.clone(),
565 author_registration: author_registration.clone(),
566 coord: StoreCommitCoord {
567 stream_id: coord.stream_id,
568 sequence,
569 },
570 },
571 superseded_entry: None,
572 retired_candidates: vec![cleanup],
573 })
574 }
575
576 pub fn membership_abandonment(
577 &self,
578 ) -> Option<&coven_protocol::prepared_commit::PreparedStoreOperationCommit> {
579 match &self.attempt {
580 ActiveStorePublicationAttempt::MembershipAbandonment { candidate } => Some(candidate),
581 _ => None,
582 }
583 }
584
585 pub(crate) fn begin_membership_abandonment(
586 &self,
587 candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
588 ) -> Result<Self, DbError> {
589 candidate.validate_closed_shape()?;
590 if self.owner != ActiveStorePublicationOwner::MembershipMutation
591 || !matches!(self.attempt, ActiveStorePublicationAttempt::Commit { .. })
592 || self.commit_reservation()
593 != Some((
594 &candidate.commit.write_id,
595 &candidate.commit.author_registration,
596 &candidate.reference.coord,
597 ))
598 || self.superseded_entry.is_some()
599 || !self.retired_candidates.is_empty()
600 {
601 return Err(DbError::Message(
602 "membership abandonment changes its operation or retains earlier cleanup".into(),
603 ));
604 }
605 Self::validated(
606 self.owner.clone(),
607 ActiveStorePublicationAttempt::MembershipAbandonment {
608 candidate: Box::new(candidate),
609 },
610 )
611 }
612
613 pub(crate) fn replace_acknowledgement_candidate(
614 &self,
615 candidate: &coven_protocol::prepared_commit::PreparedStoreOperationCommit,
616 cleanup: RetiredStoreCandidate,
617 ) -> Result<Self, DbError> {
618 if self.owner != ActiveStorePublicationOwner::StoreAcknowledgement
619 || self.commit_reservation()
620 != Some((
621 &candidate.commit.write_id,
622 &candidate.commit.author_registration,
623 &candidate.reference.coord,
624 ))
625 || self.attempt()?.entry.payload
626 != coven_protocol::store_commit::StorePublicationPayload::Commit(
627 cleanup.candidate()?,
628 )
629 || !matches!(
630 cleanup.inputs,
631 RetiredStoreCandidateInputs::Acknowledgement(_)
632 )
633 {
634 return Err(DbError::Message(
635 "acknowledgement replacement changes its reserved operation".into(),
636 ));
637 }
638 cleanup.objects()?;
639 let mut replacement = self.replace_attempt(candidate.publication.clone())?;
640 replacement.retired_candidates.push(cleanup);
641 Ok(replacement)
642 }
643
644 pub fn commit_reservation(
645 &self,
646 ) -> Option<(&WriteId, &StoreDeviceRegistrationRef, &StoreCommitCoord)> {
647 match &self.attempt {
648 ActiveStorePublicationAttempt::AwaitingPreparation {
649 write_id,
650 author_registration,
651 coord,
652 ..
653 }
654 | ActiveStorePublicationAttempt::Discarding {
655 write_id,
656 author_registration,
657 coord,
658 }
659 | ActiveStorePublicationAttempt::Commit {
660 write_id,
661 author_registration,
662 coord,
663 ..
664 } => Some((write_id, author_registration, coord)),
665 ActiveStorePublicationAttempt::MembershipAbandonment { candidate } => Some((
666 &candidate.commit.write_id,
667 &candidate.commit.author_registration,
668 &candidate.reference.coord,
669 )),
670 ActiveStorePublicationAttempt::CompletingCoveredWrite {
671 write_id, position, ..
672 } => Some((write_id, &position.author_registration, &position.coord)),
673 ActiveStorePublicationAttempt::Snapshot { .. }
674 | ActiveStorePublicationAttempt::SnapshotSuperseded { .. } => None,
675 }
676 }
677
678 pub fn superseded_entry(&self) -> Option<&coven_protocol::store_commit::StorePublicationRef> {
679 self.superseded_entry.as_ref()
680 }
681
682 pub fn same_commit_reservation(&self, other: &Self) -> bool {
683 self.commit_reservation().is_some()
684 && self.owner == other.owner
685 && self.commit_reservation() == other.commit_reservation()
686 && matches!((self.attempt(), other.attempt()), (Ok(left), Ok(right)) if left.entry.payload == right.entry.payload)
687 }
688
689 pub(crate) fn retain_superseded_entry(
690 &mut self,
691 reference: coven_protocol::store_commit::StorePublicationRef,
692 ) -> Result<(), DbError> {
693 if self.superseded_entry.is_some() || reference == self.attempt()?.reference()? {
694 return Err(DbError::Message(
695 "Store publication already owns superseded entry cleanup".to_string(),
696 ));
697 }
698 self.superseded_entry = Some(reference);
699 Ok(())
700 }
701
702 pub(crate) fn complete_superseded_entry_cleanup(&mut self) -> Result<(), DbError> {
703 if self.superseded_entry.take().is_none() {
704 return Err(DbError::Message(
705 "Store publication has no superseded entry cleanup".to_string(),
706 ));
707 }
708 Ok(())
709 }
710
711 pub fn replace_attempt(
712 &self,
713 attempt: coven_protocol::prepared_commit::PreparedStorePublication,
714 ) -> Result<Self, DbError> {
715 if self.superseded_entry.is_some() || !self.retired_snapshot_objects().is_empty() {
716 return Err(DbError::Message(
717 "Store publication must finish superseded entry cleanup before another replacement"
718 .to_string(),
719 ));
720 }
721 let replacement = match &self.attempt {
722 ActiveStorePublicationAttempt::Discarding { .. }
723 | ActiveStorePublicationAttempt::CompletingCoveredWrite { .. }
724 | ActiveStorePublicationAttempt::SnapshotSuperseded { .. } => {
725 return Err(DbError::Message(
726 "terminal operation cannot prepare another publication candidate".to_string(),
727 ));
728 }
729 ActiveStorePublicationAttempt::AwaitingPreparation {
730 write_id,
731 author_registration,
732 coord,
733 } => ActiveStorePublicationAttempt::Commit {
734 write_id: write_id.clone(),
735 author_registration: author_registration.clone(),
736 coord: coord.clone(),
737 publication: attempt,
738 },
739 ActiveStorePublicationAttempt::Commit {
740 write_id,
741 author_registration,
742 coord,
743 ..
744 } => ActiveStorePublicationAttempt::Commit {
745 write_id: write_id.clone(),
746 author_registration: author_registration.clone(),
747 coord: coord.clone(),
748 publication: attempt,
749 },
750 ActiveStorePublicationAttempt::Snapshot { .. } => {
751 ActiveStorePublicationAttempt::Snapshot {
752 publication: attempt,
753 retired_objects: Vec::new(),
754 }
755 }
756 ActiveStorePublicationAttempt::MembershipAbandonment { candidate } => {
757 let mut candidate = candidate.clone();
758 candidate.publication = attempt;
759 candidate.validate_closed_shape()?;
760 ActiveStorePublicationAttempt::MembershipAbandonment { candidate }
761 }
762 };
763 let mut replacement = Self::validated(self.owner.clone(), replacement)?;
764 replacement.retired_candidates = self.retired_candidates.clone();
765 Ok(replacement)
766 }
767}
768
769#[derive(Debug, Clone)]
770pub struct DurableDeviceRegistration {
771 pub device_id: coven_protocol::store_commit::StoreDeviceId,
772 pub registration_hash: ObjectHash,
773 pub registration_bytes: Vec<u8>,
774 pub prepared: PreparedExactObject,
775 pub initial_ack_ref: StoreAckRef,
776 pub initial_ack: ExactProtocolObject<StoreAck>,
777 pub state: LocalDeviceRegistrationState,
778}
779
780pub struct OwnerRecoveryPublication {
784 pub commit: ExactProtocolObject<coven_protocol::store_commit::VerifiedStoreBatchCommit>,
785 pub publication: coven_protocol::prepared_commit::PreparedStorePublication,
786 pub history_evidence: coven_protocol::store_commit::RetainedMergeCommitEvidence,
787}
788
789impl OwnerRecoveryPublication {
790 pub fn membership_publication(
791 &self,
792 ) -> Result<coven_protocol::membership_mutation::PreparedMembershipPublication, DbError> {
793 let proof = self
794 .history_evidence
795 .membership_proof
796 .as_ref()
797 .ok_or_else(|| {
798 DbError::Message("Owner recovery lacks its exact membership activation".into())
799 })?;
800 let publication = coven_protocol::membership_mutation::PreparedMembershipPublication {
801 entry: proof.entry_value.clone(),
802 entry_ref: proof.entry.clone(),
803 head: proof.head_value.clone(),
804 head_ref: proof.head.clone(),
805 };
806 publication.validate().map_err(|error| {
807 DbError::context(
808 "Owner recovery membership publication",
809 coven_protocol::prepared_commit::PreparedCommitError::from(error),
810 )
811 })?;
812 Ok(publication)
813 }
814
815 pub fn remote_objects(
816 &self,
817 ) -> Result<Vec<coven_protocol::remote_object::ClosedRemoteObject>, DbError> {
818 let mut objects = self
819 .membership_publication()?
820 .candidate_remote_objects(self.commit.value.value(), self.commit.value.reference())?;
821 objects.push(
822 coven_protocol::remote_object::RemoteObjectRecord::candidate_commit(
823 self.commit.value.reference().clone(),
824 &self.commit.bytes,
825 self.commit.prepared.stored_bytes(),
826 )?,
827 );
828 Ok(objects)
829 }
830}
831
832#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
833#[serde(rename_all = "snake_case", deny_unknown_fields)]
834pub enum LocalDeviceRegistrationState {
835 Prepared,
836 RegistrationPublished,
837 RegistrationActivated {
838 authority: coven_protocol::store_commit::StoreDeviceRegistrationActivation,
839 },
840 Created,
841 Activated {
842 authority: coven_protocol::store_commit::StoreDeviceRegistrationActivation,
843 },
844}
845
846pub type PreparedLocalDeviceRegistrationRow =
847 (String, String, Vec<u8>, String, String, Vec<u8>, String);
848pub type LocalDeviceRegistrationJournalRow = (
849 String,
850 String,
851 Vec<u8>,
852 String,
853 String,
854 Vec<u8>,
855 String,
856 String,
857);
858
859impl DurableDeviceRegistration {
860 pub fn is_activated(&self) -> bool {
861 matches!(self.state, LocalDeviceRegistrationState::Activated { .. })
862 }
863}
864
865#[derive(Debug, Clone)]
866pub struct DurableMembershipMutation {
867 pub intent_hash: ObjectHash,
868 pub plan_bytes: Vec<u8>,
869 pub progress_bytes: Vec<u8>,
870}
871
872pub struct DurableSnapshotPublication {
873 pub reference: StoreSnapshotRef,
874 pub meta: ExactProtocolObject<SnapshotMeta>,
875 pub publication: coven_protocol::prepared_commit::PreparedStorePublication,
876 pub rollup: ExactProtocolObject<coven_protocol::store_commit::MembershipRollup>,
880 pub image: PreparedProtocolObject<Vec<u8>>,
881 pub blobs: Vec<PreparedSnapshotBlob>,
882}
883
884pub enum StoreSnapshotPublicationStage {
885 Initial,
886 Replacing {
887 previous: StoreSnapshotRef,
888 accepted: crate::AcceptedStorePublicationInterval,
889 },
890}
891
892#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
893#[serde(deny_unknown_fields)]
894pub struct PreparedSnapshotBlob {
895 pub bindings: Vec<RowBlobLocatorBinding>,
896 pub authority: coven_protocol::audience_package::PackageAudience,
897 pub remote: RemoteObjectRecord,
898}
899
900#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
901#[serde(deny_unknown_fields)]
902pub struct PublishedStoreSnapshot {
903 pub reference: StoreSnapshotRef,
904 pub meta: SnapshotMeta,
905}
906
907pub struct DurableCircleSnapshotPublication {
908 pub reference: coven_protocol::store_commit::CircleSnapshotRef,
909 pub meta: ExactProtocolObject<coven_protocol::store_commit::CircleSnapshotMeta>,
910 pub image: PreparedProtocolObject<Vec<u8>>,
911}
912
913#[derive(Debug, Clone)]
914pub struct PublishedCircleSnapshot {
915 pub reference: coven_protocol::store_commit::CircleSnapshotRef,
916 pub successor_slot: coven_protocol::objects::ObjectSlot,
917 pub cut: coven_protocol::store_commit::CommitFrontier,
918}