1use crate::{
2 CanvasCommittedMutation, CanvasDocument, CanvasDocumentDiff, CanvasEditor, CanvasEvent,
3 CanvasPreparedMutation, CanvasRecordOperationBatch, CanvasRelationOperationBatch,
4 CanvasSnapshot, CanvasToolId, CanvasToolIntent, CanvasToolReducer, CanvasToolRegistry,
5 CanvasTransaction, DocumentError, tool::CanvasToolEffect,
6};
7use crate::{CanvasStore, CanvasStoreChange};
8use std::{convert::Infallible, error::Error, fmt};
9
10#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
11pub struct CanvasCheckpoint {
12 pub sequence: u64,
13 pub snapshot: CanvasSnapshot,
14}
15
16impl CanvasCheckpoint {
17 pub fn new(sequence: u64, document: &CanvasDocument) -> Self {
18 Self {
19 sequence,
20 snapshot: document.to_snapshot(),
21 }
22 }
23}
24
25#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
26pub struct CanvasLogEntry {
27 sequence: u64,
28 transaction: CanvasTransaction,
29 #[serde(default, alias = "record_operation_batch")]
30 committed_record_operation_batch: Option<CanvasRecordOperationBatch>,
31 #[serde(default)]
32 committed_relation_operation_batch: Option<CanvasRelationOperationBatch>,
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum CanvasLogEntryKind {
37 CommittedMutation,
38 PartialCommittedMutation,
39 LegacyReplayTransaction,
40}
41
42impl CanvasLogEntry {
43 pub fn from_replay_transaction(
44 sequence: u64,
45 transaction: impl Into<CanvasTransaction>,
46 ) -> Self {
47 Self {
48 sequence,
49 transaction: transaction.into(),
50 committed_record_operation_batch: None,
51 committed_relation_operation_batch: None,
52 }
53 }
54
55 pub fn from_committed_mutation(sequence: u64, committed: &CanvasCommittedMutation) -> Self {
56 Self {
57 sequence,
58 transaction: committed.transaction().clone(),
59 committed_record_operation_batch: Some(committed.record_operation_batch(sequence)),
60 committed_relation_operation_batch: Some(committed.relation_operation_batch(sequence)),
61 }
62 }
63
64 pub fn sequence(&self) -> u64 {
65 self.sequence
66 }
67
68 pub fn transaction(&self) -> &CanvasTransaction {
69 &self.transaction
70 }
71
72 pub fn kind(&self) -> CanvasLogEntryKind {
73 match (
74 self.committed_record_operation_batch.is_some(),
75 self.committed_relation_operation_batch.is_some(),
76 ) {
77 (true, true) => CanvasLogEntryKind::CommittedMutation,
78 (true, false) | (false, true) => CanvasLogEntryKind::PartialCommittedMutation,
79 (false, false) => CanvasLogEntryKind::LegacyReplayTransaction,
80 }
81 }
82
83 pub fn committed_record_operations(&self) -> Option<&CanvasRecordOperationBatch> {
84 self.committed_record_operation_batch.as_ref()
85 }
86
87 pub fn committed_relation_operations(&self) -> Option<&CanvasRelationOperationBatch> {
88 self.committed_relation_operation_batch.as_ref()
89 }
90
91 pub fn is_legacy_replay_entry(&self) -> bool {
92 self.kind() == CanvasLogEntryKind::LegacyReplayTransaction
93 }
94}
95
96#[derive(Debug, Eq, PartialEq)]
97pub enum CanvasPersistenceError<E = Infallible> {
98 Store(E),
99 Document(DocumentError),
100 NonMonotonicLogSequence { previous: u64, found: u64 },
101}
102
103impl<E> fmt::Display for CanvasPersistenceError<E>
104where
105 E: fmt::Display,
106{
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Self::Store(error) => write!(f, "canvas persistence store error: {error}"),
110 Self::Document(error) => fmt::Display::fmt(error, f),
111 Self::NonMonotonicLogSequence { previous, found } => write!(
112 f,
113 "canvas transaction log sequence `{found}` is not greater than previous sequence `{previous}`"
114 ),
115 }
116 }
117}
118
119impl<E> Error for CanvasPersistenceError<E> where E: fmt::Debug + fmt::Display {}
120
121impl<E> From<DocumentError> for CanvasPersistenceError<E> {
122 fn from(value: DocumentError) -> Self {
123 Self::Document(value)
124 }
125}
126
127pub type CanvasReplayError = CanvasPersistenceError<Infallible>;
128
129#[derive(Debug, Eq, PartialEq)]
130pub enum CanvasPersistentToolRegistryError<E = Infallible> {
131 MissingTool(CanvasToolId),
132 Persistence(CanvasPersistenceError<E>),
133}
134
135impl<E> From<CanvasPersistenceError<E>> for CanvasPersistentToolRegistryError<E> {
136 fn from(value: CanvasPersistenceError<E>) -> Self {
137 Self::Persistence(value)
138 }
139}
140
141impl<E> fmt::Display for CanvasPersistentToolRegistryError<E>
142where
143 E: fmt::Display,
144{
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::MissingTool(id) => write!(f, "canvas custom tool `{id}` is not registered"),
148 Self::Persistence(error) => fmt::Display::fmt(error, f),
149 }
150 }
151}
152
153impl<E> Error for CanvasPersistentToolRegistryError<E>
154where
155 E: fmt::Debug + fmt::Display + 'static,
156{
157 fn source(&self) -> Option<&(dyn Error + 'static)> {
158 match self {
159 Self::MissingTool(_) => None,
160 Self::Persistence(error) => Some(error),
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
166pub struct CanvasPersistenceCursor {
167 sequence: u64,
168}
169
170impl CanvasPersistenceCursor {
171 pub fn new(sequence: u64) -> Self {
172 Self { sequence }
173 }
174
175 pub fn sequence(&self) -> u64 {
176 self.sequence
177 }
178
179 pub fn next_sequence(&self) -> u64 {
180 self.sequence + 1
181 }
182
183 pub fn advance(&mut self) -> u64 {
184 self.sequence = self.next_sequence();
185 self.sequence
186 }
187}
188
189pub trait CanvasPersistenceStore {
190 type Error: fmt::Debug + fmt::Display;
191
192 fn load_checkpoint(&self) -> Result<Option<CanvasCheckpoint>, Self::Error>;
193
194 fn save_checkpoint(&mut self, checkpoint: CanvasCheckpoint) -> Result<(), Self::Error>;
195
196 fn append_log_entry(&mut self, entry: CanvasLogEntry) -> Result<(), Self::Error>;
197
198 fn load_log_entries(&self, after_sequence: u64) -> Result<Vec<CanvasLogEntry>, Self::Error>;
199
200 fn compact_log_entries(&mut self, through_sequence: u64) -> Result<(), Self::Error>;
201}
202
203pub fn replay_canvas_log(
204 checkpoint: Option<CanvasCheckpoint>,
205 log_entries: impl IntoIterator<Item = CanvasLogEntry>,
206) -> Result<CanvasDocument, CanvasReplayError> {
207 replay_checkpoint_and_log(checkpoint, log_entries)
208}
209
210pub fn load_canvas_document<S>(
211 store: &S,
212) -> Result<CanvasDocument, CanvasPersistenceError<S::Error>>
213where
214 S: CanvasPersistenceStore,
215{
216 let checkpoint = store
217 .load_checkpoint()
218 .map_err(CanvasPersistenceError::Store)?;
219 let after_sequence = checkpoint
220 .as_ref()
221 .map_or(0, |checkpoint| checkpoint.sequence);
222 let log_entries = store
223 .load_log_entries(after_sequence)
224 .map_err(CanvasPersistenceError::Store)?;
225
226 replay_checkpoint_and_log(checkpoint, log_entries)
227}
228
229pub fn load_canvas_persistence_cursor<S>(
230 store: &S,
231) -> Result<CanvasPersistenceCursor, CanvasPersistenceError<S::Error>>
232where
233 S: CanvasPersistenceStore,
234{
235 let checkpoint = store
236 .load_checkpoint()
237 .map_err(CanvasPersistenceError::Store)?;
238 let mut previous_sequence = checkpoint
239 .as_ref()
240 .map_or(0, |checkpoint| checkpoint.sequence);
241 let log_entries = store
242 .load_log_entries(previous_sequence)
243 .map_err(CanvasPersistenceError::Store)?;
244
245 for entry in log_entries {
246 if entry.sequence() <= previous_sequence {
247 return Err(CanvasPersistenceError::NonMonotonicLogSequence {
248 previous: previous_sequence,
249 found: entry.sequence(),
250 });
251 }
252
253 previous_sequence = entry.sequence();
254 }
255
256 Ok(CanvasPersistenceCursor::new(previous_sequence))
257}
258
259pub fn apply_persistent_transaction<S>(
260 editor: &mut CanvasEditor,
261 store: &mut S,
262 cursor: &mut CanvasPersistenceCursor,
263 transaction: CanvasTransaction,
264) -> Result<CanvasDocumentDiff, CanvasPersistenceError<S::Error>>
265where
266 S: CanvasPersistenceStore,
267{
268 let change =
269 apply_persistent_store_transaction(editor.store_mut(), store, cursor, transaction)?;
270 let diff = change
271 .as_ref()
272 .map_or_else(CanvasDocumentDiff::default, |change| change.diff().clone());
273 if change.is_some() {
274 editor.retain_selection_for_current_document();
275 }
276 Ok(diff)
277}
278
279pub fn apply_persistent_store_transaction<S>(
280 canvas_store: &mut CanvasStore,
281 store: &mut S,
282 cursor: &mut CanvasPersistenceCursor,
283 transaction: CanvasTransaction,
284) -> Result<Option<CanvasStoreChange>, CanvasPersistenceError<S::Error>>
285where
286 S: CanvasPersistenceStore,
287{
288 if transaction.is_empty() {
289 return Ok(None);
290 }
291
292 let prepared = canvas_store.prepare_transaction(transaction)?;
293 append_then_apply_prepared_mutation(
294 canvas_store,
295 store,
296 cursor,
297 prepared,
298 |canvas_store, prepared| canvas_store.apply_prepared_transaction(prepared),
299 )
300}
301
302pub fn undo_persistent_transaction<S>(
303 editor: &mut CanvasEditor,
304 store: &mut S,
305 cursor: &mut CanvasPersistenceCursor,
306) -> Result<bool, CanvasPersistenceError<S::Error>>
307where
308 S: CanvasPersistenceStore,
309{
310 let change = undo_persistent_store_transaction(editor.store_mut(), store, cursor)?;
311 if change.is_some() {
312 editor.retain_selection_for_current_document();
313 }
314 Ok(change.is_some())
315}
316
317pub fn undo_persistent_store_transaction<S>(
318 canvas_store: &mut CanvasStore,
319 store: &mut S,
320 cursor: &mut CanvasPersistenceCursor,
321) -> Result<Option<CanvasStoreChange>, CanvasPersistenceError<S::Error>>
322where
323 S: CanvasPersistenceStore,
324{
325 let Some(prepared) = canvas_store.prepare_undo()? else {
326 return Ok(None);
327 };
328 append_then_apply_prepared_mutation(
329 canvas_store,
330 store,
331 cursor,
332 prepared,
333 |canvas_store, prepared| canvas_store.apply_prepared_undo(prepared),
334 )
335}
336
337pub fn redo_persistent_transaction<S>(
338 editor: &mut CanvasEditor,
339 store: &mut S,
340 cursor: &mut CanvasPersistenceCursor,
341) -> Result<bool, CanvasPersistenceError<S::Error>>
342where
343 S: CanvasPersistenceStore,
344{
345 let change = redo_persistent_store_transaction(editor.store_mut(), store, cursor)?;
346 if change.is_some() {
347 editor.retain_selection_for_current_document();
348 }
349 Ok(change.is_some())
350}
351
352pub fn redo_persistent_store_transaction<S>(
353 canvas_store: &mut CanvasStore,
354 store: &mut S,
355 cursor: &mut CanvasPersistenceCursor,
356) -> Result<Option<CanvasStoreChange>, CanvasPersistenceError<S::Error>>
357where
358 S: CanvasPersistenceStore,
359{
360 let Some(prepared) = canvas_store.prepare_redo()? else {
361 return Ok(None);
362 };
363 append_then_apply_prepared_mutation(
364 canvas_store,
365 store,
366 cursor,
367 prepared,
368 |canvas_store, prepared| canvas_store.apply_prepared_redo(prepared),
369 )
370}
371
372pub(crate) fn apply_persistent_tool_effect<S>(
373 editor: &mut CanvasEditor,
374 store: &mut S,
375 cursor: &mut CanvasPersistenceCursor,
376 effect: CanvasToolEffect,
377) -> Result<(), CanvasPersistenceError<S::Error>>
378where
379 S: CanvasPersistenceStore,
380{
381 match effect {
382 CanvasToolEffect::ApplyTransaction(transaction) => {
383 apply_persistent_transaction(editor, store, cursor, transaction)?;
384 }
385 CanvasToolEffect::CommitGesture => {
386 apply_persistent_gesture_commit(editor, store, cursor)?;
387 }
388 effect => {
389 editor.apply_tool_effect(effect)?;
390 }
391 }
392
393 Ok(())
394}
395
396pub(crate) fn apply_persistent_tool_effects<S>(
397 editor: &mut CanvasEditor,
398 store: &mut S,
399 cursor: &mut CanvasPersistenceCursor,
400 effects: impl IntoIterator<Item = CanvasToolEffect>,
401) -> Result<(), CanvasPersistenceError<S::Error>>
402where
403 S: CanvasPersistenceStore,
404{
405 for effect in effects {
406 apply_persistent_tool_effect(editor, store, cursor, effect)?;
407 }
408
409 Ok(())
410}
411
412pub fn apply_persistent_tool_intent<S>(
413 editor: &mut CanvasEditor,
414 store: &mut S,
415 cursor: &mut CanvasPersistenceCursor,
416 intent: CanvasToolIntent,
417) -> Result<(), CanvasPersistenceError<S::Error>>
418where
419 S: CanvasPersistenceStore,
420{
421 match intent {
422 CanvasToolIntent::ApplyTransaction(transaction) => {
423 editor.apply_custom_tool_intent(CanvasToolIntent::ApplyTransaction(transaction))?;
424 }
425 CanvasToolIntent::CommitTransaction => {
426 apply_persistent_gesture_commit(editor, store, cursor)?;
427 }
428 intent => {
429 editor.apply_custom_tool_intent(intent)?;
430 }
431 }
432
433 Ok(())
434}
435
436pub fn apply_persistent_tool_intents<S>(
437 editor: &mut CanvasEditor,
438 store: &mut S,
439 cursor: &mut CanvasPersistenceCursor,
440 intents: impl IntoIterator<Item = CanvasToolIntent>,
441) -> Result<(), CanvasPersistenceError<S::Error>>
442where
443 S: CanvasPersistenceStore,
444{
445 for intent in intents {
446 apply_persistent_tool_intent(editor, store, cursor, intent)?;
447 }
448
449 Ok(())
450}
451
452pub fn handle_persistent_event<S>(
453 editor: &mut CanvasEditor,
454 store: &mut S,
455 cursor: &mut CanvasPersistenceCursor,
456 event: CanvasEvent,
457) -> Result<(), CanvasPersistenceError<S::Error>>
458where
459 S: CanvasPersistenceStore,
460{
461 let effects = editor.event_effects(event)?;
462 apply_persistent_tool_effects(editor, store, cursor, effects)
463}
464
465pub fn handle_persistent_event_with_custom_tool<S, T>(
466 editor: &mut CanvasEditor,
467 store: &mut S,
468 cursor: &mut CanvasPersistenceCursor,
469 event: CanvasEvent,
470 custom_tool: &mut T,
471) -> Result<(), CanvasPersistenceError<S::Error>>
472where
473 S: CanvasPersistenceStore,
474 T: CanvasToolReducer + ?Sized,
475{
476 if editor.tool().custom_id().is_some() {
477 let intents = custom_tool.handle_event(editor.tool_context(), event)?;
478 apply_persistent_tool_intents(editor, store, cursor, intents)
479 } else {
480 let effects = editor.event_effects(event)?;
481 apply_persistent_tool_effects(editor, store, cursor, effects)
482 }
483}
484
485pub fn handle_persistent_event_with_tool_registry<S>(
486 editor: &mut CanvasEditor,
487 store: &mut S,
488 cursor: &mut CanvasPersistenceCursor,
489 event: CanvasEvent,
490 registry: &mut CanvasToolRegistry,
491) -> Result<(), CanvasPersistentToolRegistryError<S::Error>>
492where
493 S: CanvasPersistenceStore,
494{
495 if let Some(tool_id) = editor.tool().custom_id().cloned() {
496 let reducer = registry
497 .reducer_mut(&tool_id)
498 .ok_or_else(|| CanvasPersistentToolRegistryError::MissingTool(tool_id.clone()))?;
499 let intents = reducer
500 .handle_event(editor.tool_context(), event)
501 .map_err(|error| {
502 CanvasPersistentToolRegistryError::Persistence(CanvasPersistenceError::Document(
503 error,
504 ))
505 })?;
506 apply_persistent_tool_intents(editor, store, cursor, intents)
507 .map_err(CanvasPersistentToolRegistryError::from)?;
508 } else {
509 let effects = editor.event_effects(event).map_err(|error| {
510 CanvasPersistentToolRegistryError::Persistence(CanvasPersistenceError::Document(error))
511 })?;
512 apply_persistent_tool_effects(editor, store, cursor, effects)
513 .map_err(CanvasPersistentToolRegistryError::from)?;
514 }
515 Ok(())
516}
517
518pub fn save_canvas_checkpoint<S>(
519 editor: &CanvasEditor,
520 store: &mut S,
521 cursor: &CanvasPersistenceCursor,
522) -> Result<CanvasCheckpoint, CanvasPersistenceError<S::Error>>
523where
524 S: CanvasPersistenceStore,
525{
526 save_canvas_store_checkpoint(editor.store(), store, cursor)
527}
528
529pub fn save_canvas_store_checkpoint<S>(
530 canvas_store: &CanvasStore,
531 store: &mut S,
532 cursor: &CanvasPersistenceCursor,
533) -> Result<CanvasCheckpoint, CanvasPersistenceError<S::Error>>
534where
535 S: CanvasPersistenceStore,
536{
537 let checkpoint = CanvasCheckpoint::new(cursor.sequence(), canvas_store.document());
538 store
539 .save_checkpoint(checkpoint.clone())
540 .map_err(CanvasPersistenceError::Store)?;
541 store
542 .compact_log_entries(checkpoint.sequence)
543 .map_err(CanvasPersistenceError::Store)?;
544 Ok(checkpoint)
545}
546
547fn append_committed_log_entry<S>(
548 store: &mut S,
549 cursor: &CanvasPersistenceCursor,
550 committed: &CanvasCommittedMutation,
551) -> Result<(), CanvasPersistenceError<S::Error>>
552where
553 S: CanvasPersistenceStore,
554{
555 store
556 .append_log_entry(CanvasLogEntry::from_committed_mutation(
557 cursor.next_sequence(),
558 committed,
559 ))
560 .map_err(CanvasPersistenceError::Store)
561}
562
563fn append_then_apply_prepared_mutation<S>(
564 canvas_store: &mut CanvasStore,
565 store: &mut S,
566 cursor: &mut CanvasPersistenceCursor,
567 prepared: CanvasPreparedMutation,
568 apply: impl FnOnce(&mut CanvasStore, CanvasPreparedMutation) -> Option<CanvasStoreChange>,
569) -> Result<Option<CanvasStoreChange>, CanvasPersistenceError<S::Error>>
570where
571 S: CanvasPersistenceStore,
572{
573 let committed = prepared.committed().clone();
574 if committed.diff().is_empty() {
575 return Ok(apply(canvas_store, prepared));
576 }
577
578 append_committed_log_entry(store, cursor, &committed)?;
579 let change = apply(canvas_store, prepared);
580 debug_assert!(change.is_some());
581 cursor.advance();
582 Ok(change)
583}
584
585fn apply_persistent_gesture_commit<S>(
586 editor: &mut CanvasEditor,
587 store: &mut S,
588 cursor: &mut CanvasPersistenceCursor,
589) -> Result<(), CanvasPersistenceError<S::Error>>
590where
591 S: CanvasPersistenceStore,
592{
593 match editor.prepare_gesture_commit()? {
594 Some(prepared) => {
595 let committed = prepared.committed().clone();
596 if committed.diff().is_empty() {
597 editor.apply_prepared_gesture_store_change(prepared);
598 return Ok(());
599 }
600 append_committed_log_entry(store, cursor, &committed)?;
601 let change = editor.apply_prepared_gesture_store_change(prepared);
602 debug_assert!(change.is_some());
603 cursor.advance();
604 }
605 None => {
606 editor.apply_tool_effect(CanvasToolEffect::CommitGesture)?;
607 }
608 }
609
610 Ok(())
611}
612
613fn replay_checkpoint_and_log<E>(
614 checkpoint: Option<CanvasCheckpoint>,
615 log_entries: impl IntoIterator<Item = CanvasLogEntry>,
616) -> Result<CanvasDocument, CanvasPersistenceError<E>> {
617 let mut previous_sequence = checkpoint
618 .as_ref()
619 .map_or(0, |checkpoint| checkpoint.sequence);
620 let mut document = match checkpoint {
621 Some(checkpoint) => CanvasDocument::from_snapshot(checkpoint.snapshot)?,
622 None => CanvasDocument::default(),
623 };
624
625 for entry in log_entries {
626 if entry.sequence() <= previous_sequence {
627 return Err(CanvasPersistenceError::NonMonotonicLogSequence {
628 previous: previous_sequence,
629 found: entry.sequence(),
630 });
631 }
632
633 document.apply_transaction(entry.transaction().clone())?;
634 previous_sequence = entry.sequence();
635 }
636
637 Ok(document)
638}