coven_replication/sync/store/device_join/
journal.rs1use coven_protocol::store_commit::DeviceJoinAttemptId;
2
3use super::DeviceJoinError;
4use coven_database::StoreDatabase;
5use coven_protocol::store_commit::device_join_exchange::{
6 DeviceJoinActivation, DeviceJoinReadiness,
7};
8
9pub(crate) use coven_database::device_join_journal::validate_initial_progress;
10use coven_database::device_join_journal::validate_successor;
11pub(crate) use coven_protocol::store_commit::device_join_journal::attempt_key;
12pub(crate) use coven_protocol::store_commit::device_join_journal::{
13 device_join_action, DeviceJoinRoleProgress, DeviceJoinRoleProgressKind, JoinerJoinProgress,
14 OwnerJoinProgress, PreparedDeviceJoinObject,
15};
16pub use coven_protocol::store_commit::device_join_journal::{
17 DeviceJoinAction, DeviceJoinJournalRecord, DeviceJoinRole, DeviceJoinStatus,
18};
19
20pub(super) struct StoreJoinJournal<Progress> {
24 database: StoreDatabase,
25 attempt_id: DeviceJoinAttemptId,
26 progress: std::marker::PhantomData<Progress>,
27}
28
29impl<Progress: DeviceJoinRoleProgressKind> StoreJoinJournal<Progress> {
30 pub(super) fn new(database: &StoreDatabase, attempt_id: DeviceJoinAttemptId) -> Self {
31 Self {
32 database: database.clone(),
33 attempt_id,
34 progress: std::marker::PhantomData,
35 }
36 }
37
38 pub(super) async fn load(&self) -> Result<Option<DeviceJoinJournalRecord>, DeviceJoinError> {
41 Ok(self
42 .database
43 .load_device_join(self.attempt_id, Progress::ROLE)
44 .await?)
45 }
46
47 pub(super) async fn current(&self) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
50 self.load().await?.ok_or(DeviceJoinError::JournalConflict)
51 }
52
53 pub(super) fn record(&self, progress: Progress) -> DeviceJoinJournalRecord {
54 DeviceJoinJournalRecord {
55 attempt_id: self.attempt_id,
56 progress: Box::new(progress.into()),
57 }
58 }
59
60 pub(super) async fn advance(
63 &self,
64 previous: &DeviceJoinJournalRecord,
65 progress: Progress,
66 ) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
67 let next = self.record(progress);
68 self.advance_to(previous, &next).await?;
69 Ok(next)
70 }
71
72 pub(super) async fn advance_to(
75 &self,
76 previous: &DeviceJoinJournalRecord,
77 next: &DeviceJoinJournalRecord,
78 ) -> Result<(), DeviceJoinError> {
79 Ok(self
80 .database
81 .advance_device_join(previous, next.clone())
82 .await?)
83 }
84}
85
86#[derive(Clone, Debug)]
89pub struct DeviceJoinJournalDatabase {
90 store: coven_database::DeviceJoinJournalStore,
91}
92
93impl DeviceJoinJournalDatabase {
94 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, DeviceJoinError> {
95 Self::from_store(coven_database::DeviceJoinJournalStore::open(path))
96 }
97
98 #[cfg(any(test, feature = "test-utils"))]
99 pub fn open_for_test(path: impl AsRef<std::path::Path>) -> Result<Self, DeviceJoinError> {
100 Self::from_store(coven_database::DeviceJoinJournalStore::open_for_test(path))
101 }
102
103 fn from_store(
104 store: Result<coven_database::DeviceJoinJournalStore, coven_database::DbError>,
105 ) -> Result<Self, DeviceJoinError> {
106 Ok(Self {
107 store: store.map_err(database_error)?,
108 })
109 }
110
111 pub fn begin(
112 &self,
113 record: DeviceJoinJournalRecord,
114 ) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
115 validate_initial_progress(&record.progress)?;
116 let attempt_id = attempt_key(record.attempt_id);
117 let role = record.progress.role_name();
118 let payload = serde_json::to_string(&record)?;
119 let actual = self
120 .store
121 .insert_or_load(&attempt_id, role, &payload)
122 .map_err(database_error)?;
123 let actual = serde_json::from_str::<DeviceJoinJournalRecord>(&actual)?;
124 if actual != record {
125 return Err(DeviceJoinError::JournalConflict);
126 }
127 Ok(actual)
128 }
129
130 pub fn load(
131 &self,
132 attempt_id: DeviceJoinAttemptId,
133 role: DeviceJoinRole,
134 ) -> Result<Option<DeviceJoinJournalRecord>, DeviceJoinError> {
135 let raw = self
136 .store
137 .load(&attempt_key(attempt_id), role.as_str())
138 .map_err(database_error)?;
139 let record = raw
140 .map(|value| serde_json::from_str::<DeviceJoinJournalRecord>(&value))
141 .transpose()?;
142 if record
143 .as_ref()
144 .is_some_and(|record| record.attempt_id != attempt_id || record.progress.role() != role)
145 {
146 return Err(DeviceJoinError::JournalConflict);
147 }
148 Ok(record)
149 }
150
151 pub fn records(&self) -> Result<Vec<DeviceJoinJournalRecord>, DeviceJoinError> {
152 let mut records = Vec::new();
153 for (attempt_id, role, payload) in self.store.records().map_err(database_error)? {
154 let record: DeviceJoinJournalRecord = serde_json::from_str(&payload)?;
155 if attempt_key(record.attempt_id) != attempt_id || record.progress.role_name() != role {
156 return Err(DeviceJoinError::JournalConflict);
157 }
158 records.push(record);
159 }
160 records.sort_by_key(|record| (record.attempt_id, record.progress.role()));
161 Ok(records)
162 }
163
164 pub fn actions(&self) -> Result<Vec<DeviceJoinAction>, DeviceJoinError> {
165 Ok(self
166 .records()?
167 .iter()
168 .filter_map(device_join_action)
169 .collect())
170 }
171
172 pub fn status(
173 &self,
174 attempt_id: DeviceJoinAttemptId,
175 ) -> Result<Option<DeviceJoinStatus>, DeviceJoinError> {
176 self.load(attempt_id, DeviceJoinRole::Joiner)
177 .map(|record| record.as_ref().map(DeviceJoinJournalRecord::status))
178 }
179
180 pub fn completed_joiner_readiness(
181 &self,
182 attempt_id: DeviceJoinAttemptId,
183 ) -> Result<Option<DeviceJoinReadiness>, DeviceJoinError> {
184 let Some(record) = self.load(attempt_id, DeviceJoinRole::Joiner)? else {
185 return Ok(None);
186 };
187 match &*record.progress {
188 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
189 if readiness.proof.attempt_id != attempt_id {
190 return Err(DeviceJoinError::JournalConflict);
191 }
192 Ok(Some(readiness.clone()))
193 }
194 _ => Ok(None),
195 }
196 }
197
198 pub fn observe_joiner_activation_if_pending(
199 &self,
200 activation: &DeviceJoinActivation,
201 ) -> Result<Option<DeviceJoinReadiness>, DeviceJoinError> {
202 let attempt_id = activation.attempt_id;
203 let Some(current) = self.load(attempt_id, DeviceJoinRole::Joiner)? else {
204 return Ok(None);
205 };
206 match &*current.progress {
207 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
208 let observed = DeviceJoinJournalRecord {
209 attempt_id,
210 progress: Box::new(DeviceJoinRoleProgress::Joiner(
211 JoinerJoinProgress::ActivationObserved {
212 readiness: readiness.clone(),
213 activation: activation.clone(),
214 },
215 )),
216 };
217 self.advance(¤t, observed)?;
218 Ok(Some(readiness.clone()))
219 }
220 DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved {
221 readiness,
222 activation: existing,
223 }) if existing == activation => Ok(Some(readiness.clone())),
224 _ => Err(DeviceJoinError::JournalConflict),
225 }
226 }
227
228 pub fn advance(
229 &self,
230 previous: &DeviceJoinJournalRecord,
231 next: DeviceJoinJournalRecord,
232 ) -> Result<(), DeviceJoinError> {
233 validate_successor(previous, &next)?;
234 self.swap(previous, &next)
235 }
236
237 fn swap(
240 &self,
241 previous: &DeviceJoinJournalRecord,
242 next: &DeviceJoinJournalRecord,
243 ) -> Result<(), DeviceJoinError> {
244 let previous_payload = serde_json::to_string(previous)?;
245 let next_payload = serde_json::to_string(next)?;
246 if !self
247 .store
248 .compare_and_swap(
249 &attempt_key(previous.attempt_id),
250 previous.progress.role_name(),
251 &previous_payload,
252 &next_payload,
253 )
254 .map_err(database_error)?
255 {
256 return Err(DeviceJoinError::JournalConflict);
257 }
258 Ok(())
259 }
260
261 pub(super) fn retire(&self, current: &DeviceJoinJournalRecord) -> Result<(), DeviceJoinError> {
268 let payload = serde_json::to_string(current)?;
269 if !self
270 .store
271 .compare_and_forget(
272 &attempt_key(current.attempt_id),
273 current.progress.role_name(),
274 &payload,
275 )
276 .map_err(database_error)?
277 {
278 return Err(DeviceJoinError::JournalConflict);
279 }
280 Ok(())
281 }
282}
283
284pub(super) fn database_error(error: coven_database::DbError) -> DeviceJoinError {
285 DeviceJoinError::Database(error)
286}