Skip to main content

coven_database/store/store_session/candidate_lifecycle/
abandonment.rs

1use super::*;
2use crate::store::StoreSession;
3
4impl StoreSession<'_> {
5    fn mark_candidate_cleanup_absent(&mut self, object: ExactObjectRef) -> Result<(), DbError> {
6        let conn = self.conn;
7        let object_id = remote_object_id(&object);
8        let mut remote = load_remote_object_on(conn, object_id)?;
9        if remote.cleanup_target() != Some(&object) {
10            return Err(DbError::Message(format!(
11                "remote object {object_id} is not awaiting exact cleanup"
12            )));
13        }
14        remote.mark_absent_verified().map_err(|error| {
15            DbError::context(format!("mark candidate {object_id} absent"), error)
16        })?;
17        update_remote_object_on(conn, object_id, &remote)
18    }
19
20    fn blocked_merge_candidate(
21        &mut self,
22        write_id: WriteId,
23    ) -> Result<Option<BlockedMergeCandidate>, DbError> {
24        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
25        let verified_authority = &mut *self.verified_store_authority;
26        let conn = self.conn;
27        let row: Option<(String, Option<String>)> = conn
28            .query_row(
29                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
30                [write_id.as_str()],
31                |row| Ok((row.get(0)?, row.get(1)?)),
32            )
33            .optional()
34            .map_err(DbError::from)?;
35        let Some((raw_status, raw_prepared)) = row else {
36            return Err(DbError::Message(format!("write {write_id} is absent")));
37        };
38        let status: WriteStatus = serde_json::from_str(&raw_status)
39            .map_err(|error| DbError::context("blocked Merge candidate status", error))?;
40        if !matches!(status, WriteStatus::Blocked(_)) {
41            return Err(DbError::Message(format!("write {write_id} is not blocked")));
42        }
43        let Some(raw_prepared) = raw_prepared else {
44            return Ok(None);
45        };
46        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
47            .map_err(|error| DbError::context("blocked Merge candidate preparation", error))?;
48        let PreparedStoreWriteState::Publication { .. } = &prepared else {
49            return Ok(None);
50        };
51        let candidate = parse_prepared_merge_candidate_on(records, verified_authority, &prepared)?;
52        if candidate.commit.write_id != write_id {
53            return Err(DbError::Message(
54                "blocked Merge candidate differs from its write identity".to_string(),
55            ));
56        }
57        Ok(Some(BlockedMergeCandidate {
58            commit: candidate.commit,
59            commit_bytes: candidate.canonical_signed_bytes,
60            commit_object: candidate.reference.object.clone(),
61            head: candidate.head,
62            head_object: candidate.head_object,
63        }))
64    }
65
66    fn prepared_merge_abandonment_candidates(
67        &mut self,
68        write_id: WriteId,
69    ) -> Result<Option<PreparedMergeAbandonmentCandidates>, DbError> {
70        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
71        let verified_authority = &mut *self.verified_store_authority;
72        let conn = self.conn;
73        let raw_prepared: Option<String> = conn
74            .query_row(
75                "SELECT prepared FROM store_writes WHERE write_id = ?1",
76                [write_id.as_str()],
77                |row| row.get(0),
78            )
79            .map_err(DbError::from)?;
80        let Some(raw_prepared) = raw_prepared else {
81            return Ok(None);
82        };
83        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
84            .map_err(|error| DbError::context("prepared Merge abandonment", error))?;
85        let PreparedStoreWriteState::MergeAbandonment {
86            candidate_commit,
87            candidate_head,
88            authority_commit,
89            authority_head,
90            outcome: MergeAbandonmentOutcome::Prepared,
91            ..
92        } = &prepared
93        else {
94            return Ok(None);
95        };
96        Ok(Some(PreparedMergeAbandonmentCandidates {
97            candidate: blocked_merge_candidate_from_prepared(
98                parse_prepared_merge_candidate_parts_on(
99                    records,
100                    verified_authority,
101                    candidate_commit.semantic_bytes(),
102                    candidate_commit.prepared().reference(),
103                    candidate_head.semantic_bytes(),
104                    candidate_head.prepared().reference(),
105                )?,
106            ),
107            authority: blocked_merge_candidate_from_prepared(
108                parse_prepared_merge_candidate_parts_on(
109                    records,
110                    verified_authority,
111                    authority_commit.semantic_bytes(),
112                    authority_commit.prepared().reference(),
113                    authority_head.semantic_bytes(),
114                    authority_head.prepared().reference(),
115                )?,
116            ),
117        }))
118    }
119
120    fn author_exclusion_activation_for_candidate(
121        &mut self,
122        root: coven_protocol::store_commit::StoreRootRef,
123        candidate: StoreBatchCommitRef,
124        author: StoreDeviceRegistrationRef,
125    ) -> Result<Option<AuthorExclusionActivationLocator>, DbError> {
126        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
127        author_exclusion_activation_for_candidate_on(
128            records,
129            self.verified_store_authority,
130            &root,
131            &candidate,
132            &author,
133        )
134    }
135
136    fn begin_blocked_merge_candidate_nonactivation(
137        &mut self,
138        root: coven_protocol::store_commit::StoreRootRef,
139        write_id: WriteId,
140        nonactivation: BlockedMergeCandidateNonactivation,
141    ) -> Result<(), DbError> {
142        let verified_authority = &mut *self.verified_store_authority;
143        let conn = self.conn;
144        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
145        let (raw_status, raw_prepared): (String, String) = tx
146            .query_row(
147                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
148                [write_id.as_str()],
149                |row| Ok((row.get(0)?, row.get(1)?)),
150            )
151            .map_err(DbError::from)?;
152        let status: WriteStatus = serde_json::from_str(&raw_status)
153            .map_err(|error| DbError::context("blocked Merge candidate status", error))?;
154        if !matches!(status, WriteStatus::Blocked(_)) {
155            return Err(DbError::Message(format!(
156                "Merge candidate {write_id} is not blocked"
157            )));
158        }
159        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
160            .map_err(|error| DbError::context("blocked Merge candidate preparation", error))?;
161        let PreparedStoreWriteState::Publication { .. } = &prepared else {
162            return Err(DbError::Message(
163                "author exclusion reached a non-candidate Merge preparation".to_string(),
164            ));
165        };
166        let store_transaction =
167            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
168        let candidate =
169            store_transaction.prepared_merge_candidate(verified_authority, &prepared)?;
170        store_transaction.begin_blocked_merge_candidate_nonactivation(
171            verified_authority,
172            &root,
173            &write_id,
174            &candidate,
175            &nonactivation,
176            true,
177            &[],
178        )?;
179        tx.commit().map_err(DbError::from)
180    }
181
182    fn begin_prepared_merge_abandonment_nonactivation(
183        &mut self,
184        root: coven_protocol::store_commit::StoreRootRef,
185        write_id: WriteId,
186        candidate_nonactivation: BlockedMergeCandidateNonactivation,
187        authority_nonactivation: BlockedMergeCandidateNonactivation,
188    ) -> Result<WriteStatus, DbError> {
189        let verified_authority = &mut *self.verified_store_authority;
190        let conn = self.conn;
191        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
192        let (raw_status, raw_prepared): (String, String) = tx
193            .query_row(
194                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
195                [write_id.as_str()],
196                |row| Ok((row.get(0)?, row.get(1)?)),
197            )
198            .map_err(DbError::from)?;
199        let status: WriteStatus = serde_json::from_str(&raw_status)
200            .map_err(|error| DbError::context("blocked Merge abandonment status", error))?;
201        if !matches!(status, WriteStatus::Publishing | WriteStatus::Blocked(_)) {
202            return Err(DbError::Message(
203                "Merge abandonment candidates are not publishing or blocked".to_string(),
204            ));
205        }
206        let mut prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
207            .map_err(|error| DbError::context("blocked Merge abandonment preparation", error))?;
208        let PreparedStoreWriteState::MergeAbandonment {
209            candidate_commit,
210            candidate_head,
211            authority_commit,
212            authority_head,
213            outcome,
214            ..
215        } = &mut prepared
216        else {
217            return Err(DbError::Message(
218                "author exclusion reached a non-abandonment Merge preparation".to_string(),
219            ));
220        };
221        if !matches!(outcome, MergeAbandonmentOutcome::Prepared) {
222            return Err(DbError::Message(
223                "Merge abandonment already has a publication outcome".to_string(),
224            ));
225        }
226        let store_transaction =
227            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
228        let candidate = store_transaction.prepared_merge_candidate_parts(
229            verified_authority,
230            candidate_commit.semantic_bytes(),
231            candidate_commit.prepared().reference(),
232            candidate_head.semantic_bytes(),
233            candidate_head.prepared().reference(),
234        )?;
235        let authority = store_transaction.prepared_merge_candidate_parts(
236            verified_authority,
237            authority_commit.semantic_bytes(),
238            authority_commit.prepared().reference(),
239            authority_head.semantic_bytes(),
240            authority_head.prepared().reference(),
241        )?;
242        store_transaction.begin_blocked_merge_candidate_nonactivation(
243            verified_authority,
244            &root,
245            &write_id,
246            &candidate,
247            &candidate_nonactivation,
248            true,
249            &[],
250        )?;
251        store_transaction.begin_blocked_merge_candidate_nonactivation(
252            verified_authority,
253            &root,
254            &write_id,
255            &authority,
256            &authority_nonactivation,
257            false,
258            &[],
259        )?;
260        *outcome = MergeAbandonmentOutcome::AuthorExcluded;
261        let replacement = serde_json::to_string(&prepared).map_err(|error| {
262            DbError::context("serialize excluded Merge abandonment preparation", error)
263        })?;
264        let updated = tx
265            .execute(
266                "UPDATE store_writes SET prepared = ?2
267             WHERE write_id = ?1 AND status = ?3 AND prepared = ?4",
268                rusqlite::params![write_id.as_str(), replacement, raw_status, raw_prepared],
269            )
270            .map_err(DbError::from)?;
271        if updated != 1 {
272            return Err(DbError::Message(
273                "Merge abandonment changed during author-exclusion transition".to_string(),
274            ));
275        }
276        let device_id = candidate.commit.author_registration.device_id.to_string();
277        let blocked =
278            WriteStatus::Blocked(coven_protocol::write::WriteBlock::InvalidProtocolState {
279                reason: format!(
280                    "Store author {device_id} was excluded before candidate activation"
281                ),
282            });
283        Database::set_write_status_on(&tx, &write_id, &blocked)?;
284        tx.commit().map_err(DbError::from)?;
285        Ok(blocked)
286    }
287
288    fn merge_abandonment_state(
289        &mut self,
290        write_id: WriteId,
291    ) -> Result<MergeAbandonmentState, DbError> {
292        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
293        let verified_authority = &mut *self.verified_store_authority;
294        let conn = self.conn;
295        let raw_prepared: Option<String> = conn
296            .query_row(
297                "SELECT prepared FROM store_writes WHERE write_id = ?1",
298                [write_id.as_str()],
299                |row| row.get(0),
300            )
301            .map_err(DbError::from)?;
302        let Some(raw_prepared) = raw_prepared else {
303            return Ok(MergeAbandonmentState::None);
304        };
305        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
306            .map_err(|error| DbError::context("prepared Merge abandonment", error))?;
307        let PreparedStoreWriteState::MergeAbandonment {
308            candidate_commit,
309            candidate_head,
310            outcome,
311            ..
312        } = &prepared
313        else {
314            return Ok(MergeAbandonmentState::None);
315        };
316        let candidate = parse_prepared_merge_candidate_parts_on(
317            records,
318            verified_authority,
319            candidate_commit.semantic_bytes(),
320            candidate_commit.prepared().reference(),
321            candidate_head.semantic_bytes(),
322            candidate_head.prepared().reference(),
323        )?;
324        Ok(match outcome {
325            MergeAbandonmentOutcome::Prepared => MergeAbandonmentState::Prepared,
326            MergeAbandonmentOutcome::Accepted { .. } => MergeAbandonmentState::Accepted,
327            MergeAbandonmentOutcome::Lost { winner_commit, .. }
328                if winner_commit == &candidate.reference =>
329            {
330                MergeAbandonmentState::CandidateWon
331            }
332            MergeAbandonmentOutcome::Lost { .. } => MergeAbandonmentState::OtherWon,
333            MergeAbandonmentOutcome::AuthorExcluded => MergeAbandonmentState::AuthorExcluded,
334        })
335    }
336
337    fn resume_winning_merge_candidate(&mut self, write_id: WriteId) -> Result<(), DbError> {
338        let verified_authority = &mut *self.verified_store_authority;
339        let conn = self.conn;
340        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
341        let (raw_status, raw_prepared): (String, String) = tx
342            .query_row(
343                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
344                [write_id.as_str()],
345                |row| Ok((row.get(0)?, row.get(1)?)),
346            )
347            .map_err(DbError::from)?;
348        let status: WriteStatus = serde_json::from_str(&raw_status)
349            .map_err(|error| DbError::context("winning Merge candidate status", error))?;
350        if !matches!(status, WriteStatus::Blocked(_)) {
351            return Err(DbError::Message(
352                "winning Merge candidate is not blocked".to_string(),
353            ));
354        }
355        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
356            .map_err(|error| DbError::context("winning Merge candidate preparation", error))?;
357        let PreparedStoreWriteState::MergeAbandonment {
358            candidate_commit,
359            candidate_head,
360            candidate_history_evidence,
361            authority_commit,
362            authority_head,
363            authority_history_evidence: _,
364            outcome: MergeAbandonmentOutcome::Lost { winner_commit, .. },
365            local_cleanup,
366            completion,
367        } = prepared
368        else {
369            return Err(DbError::Message(
370                "Merge abandonment did not lose to its prepared candidate".to_string(),
371            ));
372        };
373        let store_transaction =
374            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
375        let candidate = store_transaction.prepared_merge_candidate_parts(
376            verified_authority,
377            candidate_commit.semantic_bytes(),
378            candidate_commit.prepared().reference(),
379            candidate_head.semantic_bytes(),
380            candidate_head.prepared().reference(),
381        )?;
382        if winner_commit != candidate.reference {
383            return Err(DbError::Message(
384                "Merge abandonment winner is another candidate".to_string(),
385            ));
386        }
387        let authority = store_transaction.prepared_merge_candidate_parts(
388            verified_authority,
389            authority_commit.semantic_bytes(),
390            authority_commit.prepared().reference(),
391            authority_head.semantic_bytes(),
392            authority_head.prepared().reference(),
393        )?;
394        remove_cleaned_merge_authority_on(&tx, &authority)?;
395        let replacement = PreparedStoreWriteState::Publication {
396            commit: candidate_commit,
397            head: candidate_head,
398            history_evidence: candidate_history_evidence,
399            local_cleanup,
400            completion,
401        };
402        let replacement = serde_json::to_string(&replacement)
403            .map_err(|error| DbError::context("serialize winning Merge candidate", error))?;
404        let publishing = serde_json::to_string(&WriteStatus::Publishing)
405            .map_err(|error| DbError::context("serialize winning Merge status", error))?;
406        let updated = tx
407            .execute(
408                "UPDATE store_writes SET prepared = ?2, status = ?3
409             WHERE write_id = ?1 AND prepared = ?4",
410                rusqlite::params![write_id.as_str(), replacement, publishing, raw_prepared],
411            )
412            .map_err(DbError::from)?;
413        if updated != 1 {
414            return Err(DbError::Message(
415                "Merge abandonment changed while restoring its winner".to_string(),
416            ));
417        }
418        tx.commit().map_err(DbError::from)?;
419        Ok(())
420    }
421
422    fn finish_lost_merge_abandonment(&mut self, write_id: WriteId) -> Result<(), DbError> {
423        let verified_authority = &mut *self.verified_store_authority;
424        let conn = self.conn;
425        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
426        let (raw_status, raw_prepared): (String, String) = tx
427            .query_row(
428                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
429                [write_id.as_str()],
430                |row| Ok((row.get(0)?, row.get(1)?)),
431            )
432            .map_err(DbError::from)?;
433        let status: WriteStatus = serde_json::from_str(&raw_status)
434            .map_err(|error| DbError::context("lost Merge abandonment status", error))?;
435        if !matches!(status, WriteStatus::Blocked(_)) {
436            return Err(DbError::Message(
437                "lost Merge abandonment is not blocked".to_string(),
438            ));
439        }
440        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
441            .map_err(|error| DbError::context("lost Merge abandonment preparation", error))?;
442        let PreparedStoreWriteState::MergeAbandonment {
443            candidate_commit,
444            candidate_head,
445            candidate_history_evidence,
446            authority_commit,
447            authority_head,
448            authority_history_evidence: _,
449            outcome: MergeAbandonmentOutcome::Lost { winner_commit, .. },
450            local_cleanup,
451            completion,
452        } = prepared
453        else {
454            return Err(DbError::Message(
455                "Merge abandonment has no third-candidate winner".to_string(),
456            ));
457        };
458        let store_transaction =
459            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
460        let candidate = store_transaction.prepared_merge_candidate_parts(
461            verified_authority,
462            candidate_commit.semantic_bytes(),
463            candidate_commit.prepared().reference(),
464            candidate_head.semantic_bytes(),
465            candidate_head.prepared().reference(),
466        )?;
467        if winner_commit == candidate.reference {
468            return Err(DbError::Message(
469                "original candidate won the Merge abandonment race".to_string(),
470            ));
471        }
472        let authority = store_transaction.prepared_merge_candidate_parts(
473            verified_authority,
474            authority_commit.semantic_bytes(),
475            authority_commit.prepared().reference(),
476            authority_head.semantic_bytes(),
477            authority_head.prepared().reference(),
478        )?;
479        remove_cleaned_merge_authority_on(&tx, &authority)?;
480        let replacement = PreparedStoreWriteState::Publication {
481            commit: candidate_commit,
482            head: candidate_head,
483            history_evidence: candidate_history_evidence,
484            local_cleanup,
485            completion,
486        };
487        let replacement = serde_json::to_string(&replacement)
488            .map_err(|error| DbError::context("serialize lost Merge candidate", error))?;
489        let updated = tx
490            .execute(
491                "UPDATE store_writes SET prepared = ?2
492             WHERE write_id = ?1 AND status = ?3 AND prepared = ?4",
493                rusqlite::params![write_id.as_str(), replacement, raw_status, raw_prepared],
494            )
495            .map_err(DbError::from)?;
496        if updated != 1 {
497            return Err(DbError::Message(
498                "Merge abandonment changed while removing its losing authority".to_string(),
499            ));
500        }
501        tx.commit().map_err(DbError::from)
502    }
503
504    fn finish_author_excluded_merge_abandonment(
505        &mut self,
506        write_id: WriteId,
507    ) -> Result<(), DbError> {
508        let verified_authority = &mut *self.verified_store_authority;
509        let conn = self.conn;
510        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
511        let (raw_status, raw_prepared): (String, String) = tx
512            .query_row(
513                "SELECT status, prepared FROM store_writes WHERE write_id = ?1",
514                [write_id.as_str()],
515                |row| Ok((row.get(0)?, row.get(1)?)),
516            )
517            .map_err(DbError::from)?;
518        let status: WriteStatus = serde_json::from_str(&raw_status)
519            .map_err(|error| DbError::context("excluded Merge abandonment status", error))?;
520        if !matches!(status, WriteStatus::Blocked(_)) {
521            return Err(DbError::Message(
522                "excluded Merge abandonment is not blocked".to_string(),
523            ));
524        }
525        let prepared: PreparedStoreWriteState = serde_json::from_str(&raw_prepared)
526            .map_err(|error| DbError::context("excluded Merge abandonment preparation", error))?;
527        let PreparedStoreWriteState::MergeAbandonment {
528            candidate_commit,
529            candidate_head,
530            candidate_history_evidence,
531            authority_commit,
532            authority_head,
533            authority_history_evidence: _,
534            outcome: MergeAbandonmentOutcome::AuthorExcluded,
535            local_cleanup,
536            completion,
537        } = prepared
538        else {
539            return Err(DbError::Message(
540                "Merge abandonment has no author-exclusion outcome".to_string(),
541            ));
542        };
543        let store_transaction =
544            crate::store::store_session::StoreTransaction::new(&tx, self.store_dir);
545        let candidate = store_transaction.prepared_merge_candidate_parts(
546            verified_authority,
547            candidate_commit.semantic_bytes(),
548            candidate_commit.prepared().reference(),
549            candidate_head.semantic_bytes(),
550            candidate_head.prepared().reference(),
551        )?;
552        let authority = store_transaction.prepared_merge_candidate_parts(
553            verified_authority,
554            authority_commit.semantic_bytes(),
555            authority_commit.prepared().reference(),
556            authority_head.semantic_bytes(),
557            authority_head.prepared().reference(),
558        )?;
559        if !merge_candidate_cleanup_targets_on(&tx, &write_id, &candidate, true, &[])?.is_empty()
560            || !merge_candidate_cleanup_targets_on(&tx, &write_id, &authority, false, &[])?
561                .is_empty()
562        {
563            return Err(DbError::Message(
564                "excluded Merge abandonment cleanup is incomplete".to_string(),
565            ));
566        }
567        remove_cleaned_author_excluded_merge_authority_on(&tx, &authority)?;
568        let replacement = PreparedStoreWriteState::Publication {
569            commit: candidate_commit,
570            head: candidate_head,
571            history_evidence: candidate_history_evidence,
572            local_cleanup,
573            completion,
574        };
575        let replacement = serde_json::to_string(&replacement).map_err(|error| {
576            DbError::context("serialize cleaned excluded Merge abandonment", error)
577        })?;
578        let updated = tx
579            .execute(
580                "UPDATE store_writes SET prepared = ?2
581             WHERE write_id = ?1 AND status = ?3 AND prepared = ?4",
582                rusqlite::params![write_id.as_str(), replacement, raw_status, raw_prepared],
583            )
584            .map_err(DbError::from)?;
585        if updated != 1 {
586            return Err(DbError::Message(
587                "excluded Merge abandonment changed during cleanup".to_string(),
588            ));
589        }
590        tx.commit().map_err(DbError::from)
591    }
592}
593
594impl StoreDatabase {
595    pub async fn mark_candidate_cleanup_absent(
596        &self,
597        object: ExactObjectRef,
598    ) -> Result<(), DbError> {
599        self.call_store(move |session| session.mark_candidate_cleanup_absent(object))
600            .await
601    }
602
603    pub async fn blocked_merge_candidate(
604        &self,
605        write_id: WriteId,
606    ) -> Result<Option<BlockedMergeCandidate>, DbError> {
607        self.call_store(move |session| session.blocked_merge_candidate(write_id))
608            .await
609    }
610
611    pub async fn prepared_merge_abandonment_candidates(
612        &self,
613        write_id: WriteId,
614    ) -> Result<Option<PreparedMergeAbandonmentCandidates>, DbError> {
615        self.call_store(move |session| session.prepared_merge_abandonment_candidates(write_id))
616            .await
617    }
618
619    pub async fn author_exclusion_activation_for_candidate(
620        &self,
621        root: coven_protocol::store_commit::StoreRootRef,
622        candidate: StoreBatchCommitRef,
623        author: StoreDeviceRegistrationRef,
624    ) -> Result<Option<AuthorExclusionActivationLocator>, DbError> {
625        self.call_store(move |session| {
626            session.author_exclusion_activation_for_candidate(root, candidate, author)
627        })
628        .await
629    }
630
631    pub async fn begin_blocked_merge_candidate_nonactivation(
632        &self,
633        root: coven_protocol::store_commit::StoreRootRef,
634        write_id: WriteId,
635        nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
636    ) -> Result<(), DbError> {
637        let nonactivation = blocked_merge_candidate_nonactivation(nonactivation)?;
638        self.call_store(move |session| {
639            session.begin_blocked_merge_candidate_nonactivation(root, write_id, nonactivation)
640        })
641        .await
642    }
643
644    pub async fn begin_prepared_merge_abandonment_nonactivation(
645        &self,
646        root: coven_protocol::store_commit::StoreRootRef,
647        write_id: WriteId,
648        candidate_nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
649        authority_nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
650    ) -> Result<(), DbError> {
651        let candidate_nonactivation =
652            blocked_merge_candidate_nonactivation(candidate_nonactivation)?;
653        let authority_nonactivation =
654            blocked_merge_candidate_nonactivation(authority_nonactivation)?;
655        let notified_write_id = write_id.clone();
656        let blocked = self
657            .call_store(move |session| {
658                session.begin_prepared_merge_abandonment_nonactivation(
659                    root,
660                    write_id,
661                    candidate_nonactivation,
662                    authority_nonactivation,
663                )
664            })
665            .await?;
666        self.notify_write_status(notified_write_id, blocked);
667        Ok(())
668    }
669
670    pub async fn merge_abandonment_state(
671        &self,
672        write_id: &WriteId,
673    ) -> Result<MergeAbandonmentState, DbError> {
674        let write_id = write_id.clone();
675        self.call_store(move |session| session.merge_abandonment_state(write_id))
676            .await
677    }
678
679    pub async fn resume_winning_merge_candidate(&self, write_id: WriteId) -> Result<(), DbError> {
680        let notified_write_id = write_id.clone();
681        self.call_store(move |session| session.resume_winning_merge_candidate(write_id))
682            .await?;
683        self.notify_write_status(notified_write_id, WriteStatus::Publishing);
684        Ok(())
685    }
686
687    pub async fn finish_lost_merge_abandonment(&self, write_id: WriteId) -> Result<(), DbError> {
688        self.call_store(move |session| session.finish_lost_merge_abandonment(write_id))
689            .await
690    }
691
692    pub async fn finish_author_excluded_merge_abandonment(
693        &self,
694        write_id: WriteId,
695    ) -> Result<(), DbError> {
696        self.call_store(move |session| session.finish_author_excluded_merge_abandonment(write_id))
697            .await
698    }
699}