1use std::collections::VecDeque;
7
8use crate::doc_ng::{
9 block_model::Live,
10 commit::Commit,
11 document::{Document, FoldError},
12 entity::Entity,
13 opcode::{Crud, OpCodes},
14 rev::{Confirmed, Provisional, Rev, RevKind},
15};
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
20pub struct Nonce(u64);
21
22#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
23pub enum SessionError {
24 #[error("expected rev {expected:?} on this connection, got {got:?}")]
26 RevGap { expected: Rev, got: Rev },
27 #[error("expected an answer for {expected:?}, got one for {got:?}")]
29 NonceMismatch { expected: Option<Nonce>, got: Nonce },
30 #[error("a sequenced commit was refused: {0}")]
33 Corrupt(#[from] FoldError),
34}
35
36#[derive(Default)]
38pub struct Host {
39 state: Document<Confirmed>,
40 log: Vec<Commit>,
41}
42
43impl Host {
44 pub fn ingest(&mut self, commit: &Commit) -> Result<Rev, FoldError> {
47 self.state = self.state.try_apply(commit)?;
48 self.log.push(commit.clone());
49 Ok(self.state.rev())
50 }
51
52 pub fn state(&self) -> &Document<Confirmed> {
53 &self.state
54 }
55
56 pub fn rev(&self) -> Rev {
57 self.state.rev()
58 }
59
60 pub fn commits_after(&self, rev: Rev) -> &[Commit] {
63 let already_held = (rev.get() as usize).min(self.log.len());
64 &self.log[already_held..]
65 }
66}
67
68struct Pending {
69 nonce: Nonce,
70 commit: Commit,
71}
72
73#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78enum JournalAs {
79 Edit,
80 Undo,
81 Redo,
82}
83
84struct JournalEntry {
85 inverse: Commit,
86 unacked: Option<Nonce>,
88}
89
90#[derive(Default)]
91struct Journal {
92 undo: Vec<JournalEntry>,
93 redo: Vec<JournalEntry>,
94}
95
96impl Journal {
97 fn push(&mut self, entry: JournalEntry, journal_as: JournalAs) {
98 match journal_as {
99 JournalAs::Edit => {
100 self.redo.clear();
101 self.undo.push(entry);
102 }
103 JournalAs::Undo => self.redo.push(entry),
104 JournalAs::Redo => self.undo.push(entry),
105 }
106 }
107
108 fn ack(&mut self, nonce: Nonce) {
109 for entry in self.undo.iter_mut().chain(self.redo.iter_mut()) {
110 if entry.unacked == Some(nonce) {
111 entry.unacked = None;
112 }
113 }
114 }
115
116 fn discard(&mut self, nonce: Nonce) {
119 self.undo.retain(|entry| entry.unacked != Some(nonce));
120 self.redo.retain(|entry| entry.unacked != Some(nonce));
121 }
122}
123
124#[derive(Default)]
126pub struct ClientSession {
127 confirmed: Document<Confirmed>,
128 pending: VecDeque<Pending>,
130 optimistic: Document<Provisional>,
132 journal: Journal,
133 next_nonce: u64,
134}
135
136impl ClientSession {
137 pub fn welcome(commits: &[Commit]) -> Result<Self, SessionError> {
140 let mut session = Self::default();
141 for commit in commits {
142 session.confirmed = session.confirmed.try_apply(commit)?;
143 }
144 session.rebuild();
145 Ok(session)
146 }
147
148 pub fn rev(&self) -> Rev {
150 self.confirmed.rev()
151 }
152
153 pub fn confirmed(&self) -> &Document<Confirmed> {
154 &self.confirmed
155 }
156
157 pub fn optimistic(&self) -> &Document<Provisional> {
158 &self.optimistic
159 }
160
161 pub fn last_submission(&self) -> Option<(Nonce, &Commit)> {
164 self.pending
165 .back()
166 .map(|pending| (pending.nonce, &pending.commit))
167 }
168
169 pub fn can_undo(&self) -> bool {
170 !self.journal.undo.is_empty()
171 }
172
173 pub fn can_redo(&self) -> bool {
174 !self.journal.redo.is_empty()
175 }
176
177 pub fn submit(&mut self, commit: Commit) -> Result<Nonce, FoldError> {
181 self.submit_journaled(commit, JournalAs::Edit)
182 }
183
184 fn submit_journaled(
185 &mut self,
186 commit: Commit,
187 journal_as: JournalAs,
188 ) -> Result<Nonce, FoldError> {
189 let pre_image = &self.optimistic;
192 let predicted = pre_image.try_apply(&commit)?;
193 let inverse = inverse_of(pre_image, &commit);
194
195 let nonce = Nonce(self.next_nonce);
196 self.next_nonce += 1;
197 self.journal.push(
198 JournalEntry {
199 inverse,
200 unacked: Some(nonce),
201 },
202 journal_as,
203 );
204 self.pending.push_back(Pending { nonce, commit });
205 self.optimistic = predicted;
207 Ok(nonce)
208 }
209
210 pub fn undo(&mut self) -> Option<Result<Nonce, FoldError>> {
216 let inverse = self.journal.undo.last()?.inverse.clone();
217 let submitted = self.submit_journaled(inverse, JournalAs::Undo);
218 if submitted.is_ok() {
219 self.journal.undo.pop();
220 }
221 Some(submitted)
222 }
223
224 pub fn redo(&mut self) -> Option<Result<Nonce, FoldError>> {
225 let inverse = self.journal.redo.last()?.inverse.clone();
226 let submitted = self.submit_journaled(inverse, JournalAs::Redo);
227 if submitted.is_ok() {
228 self.journal.redo.pop();
229 }
230 Some(submitted)
231 }
232
233 pub fn apply_foreign(&mut self, rev: Rev, commit: &Commit) -> Result<(), SessionError> {
239 self.expect_next(rev)?;
240 self.confirmed = self.confirmed.try_apply(commit)?;
241 self.rebuild();
242 Ok(())
243 }
244
245 pub fn committed(&mut self, nonce: Nonce, rev: Rev) -> Result<(), SessionError> {
251 self.expect_front(nonce)?;
252 self.expect_next(rev)?;
253 if let Some(acked) = self.pending.front() {
254 self.confirmed = self.confirmed.try_apply(&acked.commit)?;
255 self.pending.pop_front();
256 self.journal.ack(nonce);
257 self.rebuild();
258 }
259 Ok(())
260 }
261
262 pub fn rejected(&mut self, nonce: Nonce) -> Result<(), SessionError> {
267 self.expect_front(nonce)?;
268 self.pending.pop_front();
269 self.journal.discard(nonce);
270 self.rebuild();
271 Ok(())
272 }
273
274 fn expect_next(&self, rev: Rev) -> Result<(), SessionError> {
275 let expected = self.confirmed.rev().next();
276 debug_assert_eq!(rev, expected, "a gap in the per-connection rev sequence");
277 if rev == expected {
278 Ok(())
279 } else {
280 Err(SessionError::RevGap { expected, got: rev })
281 }
282 }
283
284 fn expect_front(&self, nonce: Nonce) -> Result<(), SessionError> {
285 let front = self.pending.front().map(|pending| pending.nonce);
286 if front == Some(nonce) {
287 Ok(())
288 } else {
289 Err(SessionError::NonceMismatch {
290 expected: front,
291 got: nonce,
292 })
293 }
294 }
295
296 fn rebuild(&mut self) {
299 self.optimistic = self
300 .pending
301 .iter()
302 .fold(self.confirmed.predict(), |doc, pending| {
303 match doc.try_apply(&pending.commit) {
304 Ok(predicted) => predicted,
305 Err(_) => doc,
310 }
311 });
312 }
313}
314
315fn inverse_of<R: RevKind>(pre_image: &Document<R>, commit: &Commit) -> Commit {
323 let mut ops: Vec<OpCodes> = commit
324 .ops()
325 .iter()
326 .filter_map(|op| invert_op(pre_image, op))
327 .collect();
328 ops.reverse();
329 Commit::new(format!("Undo {}", commit.label()), ops)
330}
331
332fn invert_op<R: RevKind>(pre_image: &Document<R>, op: &OpCodes) -> Option<OpCodes> {
333 match op {
334 OpCodes::Document(update) => {
335 Some(OpCodes::Document(pre_image.title_block().invert(update)))
336 }
337 OpCodes::Block(id, crud) => {
338 invert_crud(pre_image.block(id), crud).map(|crud| OpCodes::Block(*id, crud))
339 }
340 OpCodes::Pin(id, crud) => {
341 invert_crud(pre_image.pin(id), crud).map(|crud| OpCodes::Pin(*id, crud))
342 }
343 OpCodes::Route(id, crud) => {
344 invert_crud(pre_image.route(id), crud).map(|crud| OpCodes::Route(*id, crud))
345 }
346 OpCodes::RouteLabel(id, crud) => {
347 invert_crud(pre_image.route_label(id), crud).map(|crud| OpCodes::RouteLabel(*id, crud))
348 }
349 OpCodes::Text(id, crud) => {
350 invert_crud(pre_image.text(id), crud).map(|crud| OpCodes::Text(*id, crud))
351 }
352 OpCodes::Comment(id, crud) => {
353 invert_crud(pre_image.comment(id), crud).map(|crud| OpCodes::Comment(*id, crud))
354 }
355 OpCodes::Image(id, crud) => {
356 invert_crud(pre_image.image(id), crud).map(|crud| OpCodes::Image(*id, crud))
357 }
358 }
359}
360
361fn invert_crud<E: Entity>(
367 pre_image: Option<&Live<E>>,
368 crud: &Crud<E::Init, E::Update>,
369) -> Option<Crud<E::Init, E::Update>> {
370 match crud {
371 Crud::Update(update) => Some(Crud::Update(pre_image?.as_ref().invert(update))),
372 lifecycle => lifecycle.invert_lifecycle(),
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::doc_ng::fixtures::{block_id, commit, pin_id, projection};
380 use crate::doc_ng::{
381 block_model::{BlockInit, BlockUpdate, Icon, LabelInit, PinInit},
382 document::TitleBlockUpdate,
383 geometry::{FracVal, GridPoint, GridRect, GridSize},
384 id::BlockId,
385 values::{LabelSide, PinDir, Role},
386 };
387
388 fn rect(x: i32, y: i32) -> GridRect {
389 GridRect {
390 top_left: GridPoint { x, y },
391 size: GridSize { w: 4, h: 4 },
392 }
393 }
394
395 fn label_init(name: String) -> LabelInit {
396 LabelInit {
397 name,
398 side: LabelSide::default(),
399 offset: FracVal::default(),
400 hidden: false,
401 }
402 }
403
404 fn block_create(byte: u8) -> OpCodes {
405 OpCodes::Block(
406 block_id(byte),
407 Crud::Create(BlockInit {
408 parent: BlockId::NULL,
409 rect: rect(0, 0),
410 locked: false,
411 title: label_init(format!("b{byte}")),
412 type_label: label_init(String::new()),
413 icon: Icon::default(),
414 }),
415 )
416 }
417
418 fn reparent(child: u8, parent: BlockId) -> OpCodes {
419 OpCodes::Block(block_id(child), Crud::Update(BlockUpdate::Parent(parent)))
420 }
421
422 fn resize(byte: u8, to: GridRect) -> OpCodes {
423 OpCodes::Block(block_id(byte), Crud::Update(BlockUpdate::Rect(to)))
424 }
425
426 fn orphan_pin(byte: u8) -> OpCodes {
429 OpCodes::Pin(
430 pin_id(byte),
431 Crud::Create(PinInit {
432 owner: block_id(200),
433 name: format!("p{byte}"),
434 type_name: String::new(),
435 tag: String::new(),
436 tag_hidden: false,
437 rect: rect(0, 0),
438 dir: PinDir::default(),
439 pin_accent: Role::default(),
440 port_accent: Role::default(),
441 port_pin_accent: Role::default(),
442 flip_lr: false,
443 }),
444 )
445 }
446
447 fn parent_of(session: &ClientSession, byte: u8) -> BlockId {
448 *session
449 .optimistic()
450 .block(&block_id(byte))
451 .expect("the block is in the document")
452 .as_ref()
453 .parent
454 .as_ref()
455 }
456
457 fn ack_last(host: &mut Host, session: &mut ClientSession) {
460 let (nonce, commit) = session.last_submission().expect("a submission is queued");
461 let commit = commit.clone();
462 let rev = host.ingest(&commit).expect("the host accepts it");
463 session.committed(nonce, rev).expect("the ack lands");
464 }
465
466 fn edit(host: &mut Host, session: &mut ClientSession, commit: Commit) {
467 session.submit(commit).expect("the edit folds");
468 ack_last(host, session);
469 }
470
471 fn one_block() -> (Host, ClientSession) {
473 let (mut host, mut session) = (Host::default(), ClientSession::default());
474 edit(
475 &mut host,
476 &mut session,
477 commit("Added a block", vec![block_create(1)]),
478 );
479 (host, session)
480 }
481
482 #[test]
483 fn an_ack_leaves_the_prediction_equal_to_the_confirmed_document() {
484 let (host, session) = one_block();
485
486 assert!(session.last_submission().is_none(), "the queue drains");
487 assert_eq!(
488 session.optimistic().content_hash(),
489 session.confirmed().content_hash(),
490 "with nothing pending the prediction is the confirmed document",
491 );
492 assert_eq!(
493 session.confirmed().content_hash(),
494 host.state().content_hash(),
495 "and matches the host's fold, revs included",
496 );
497 }
498
499 #[test]
503 fn a_foreign_commit_rebuilds_the_prediction_over_the_new_head() {
504 let (mut host, mut session) = one_block();
505 session
506 .submit(commit("Moved it", vec![resize(1, rect(5, 5))]))
507 .expect("the edit folds");
508
509 let foreign = commit("Added another", vec![block_create(2)]);
510 let rev = host.ingest(&foreign).expect("the host accepts it");
511 session
512 .apply_foreign(rev, &foreign)
513 .expect("the delivery lands");
514
515 let pending = session
516 .last_submission()
517 .expect("the edit is still in flight")
518 .1
519 .clone();
520 let from_scratch = session
521 .confirmed()
522 .try_apply(&pending)
523 .expect("the pending edit folds on the new head");
524 assert_eq!(
525 session.optimistic().content_hash(),
526 from_scratch.content_hash(),
527 "optimistic == confirmed ⊕ pending, byte for byte",
528 );
529 assert_eq!(
530 *session
531 .optimistic()
532 .block(&block_id(1))
533 .expect("block 1 is present")
534 .as_ref()
535 .rect
536 .as_ref(),
537 rect(5, 5),
538 "the unacked local edit still stands above the foreign one",
539 );
540 }
541
542 #[test]
543 #[should_panic(expected = "gap in the per-connection rev sequence")]
544 fn a_rev_gap_panics_in_debug() {
545 let (_host, mut session) = one_block();
546 let skipped = session.rev().next().next();
547 let _ = session.apply_foreign(skipped, &commit("Skipped ahead", vec![block_create(9)]));
548 }
549
550 #[test]
551 fn an_answer_for_anything_but_the_front_of_the_queue_is_refused() {
552 let (host, mut session) = one_block();
553 let first = session
554 .submit(commit("First", vec![resize(1, rect(1, 1))]))
555 .expect("the edit folds");
556 let second = session
557 .submit(commit("Second", vec![resize(1, rect(2, 2))]))
558 .expect("the edit folds");
559 assert_ne!(first, second, "each submission gets its own nonce");
560
561 assert_eq!(
562 session.committed(second, host.rev().next()),
563 Err(SessionError::NonceMismatch {
564 expected: Some(first),
565 got: second,
566 }),
567 "answers arrive in submission order",
568 );
569 }
570
571 #[test]
572 fn a_local_edit_that_cannot_fold_is_never_queued() {
573 let mut session = ClientSession::default();
574 let before = session.optimistic().content_hash();
575
576 assert!(
577 session
578 .submit(commit("Orphan pin", vec![orphan_pin(3)]))
579 .is_err(),
580 "the client refuses what the server would refuse",
581 );
582 assert!(session.last_submission().is_none(), "nothing was queued");
583 assert!(!session.can_undo(), "and nothing was journalled");
584 assert_eq!(
585 session.optimistic().content_hash(),
586 before,
587 "the prediction is untouched",
588 );
589 }
590
591 #[test]
592 fn a_rejection_drops_the_edit_and_its_journal_entry() {
593 let (mut host, mut session) = one_block();
594 let confirmed = session.confirmed().content_hash();
595
596 let doomed = session
597 .submit(commit("Moved it", vec![resize(1, rect(5, 5))]))
598 .expect("the edit folds locally");
599 session.rejected(doomed).expect("the rejection lands");
600
601 assert_eq!(
602 session.optimistic().content_hash(),
603 confirmed,
604 "the edit visibly reverts",
605 );
606 assert!(session.can_undo(), "the accepted create is still undoable");
607
608 session
612 .undo()
613 .expect("something to undo")
614 .expect("the inverse folds");
615 ack_last(&mut host, &mut session);
616 assert!(
617 projection(session.confirmed()).is_empty(),
618 "undo removed the block, so the entry inverted the create",
619 );
620 }
621
622 #[test]
626 fn a_pending_commit_the_head_invalidated_is_skipped_then_returns() {
627 let (mut host, mut session) = (Host::default(), ClientSession::default());
628 let setup = commit("Two blocks", vec![block_create(1), block_create(2)]);
629 let rev = host.ingest(&setup).expect("the host accepts it");
630 session.apply_foreign(rev, &setup).expect("the setup lands");
631
632 let nonce = session
633 .submit(commit("Nested 1 under 2", vec![reparent(1, block_id(2))]))
634 .expect("the edit folds on today's head");
635 assert_eq!(parent_of(&session, 1), block_id(2), "predicted immediately");
636
637 let cycle = commit("Nested 2 under 1", vec![reparent(2, block_id(1))]);
638 let rev = host.ingest(&cycle).expect("no cycle at the host yet");
639 session
640 .apply_foreign(rev, &cycle)
641 .expect("the delivery lands");
642 assert_eq!(
643 parent_of(&session, 1),
644 BlockId::NULL,
645 "the pending edit would close a cycle, so it is not predicted",
646 );
647
648 let undo_cycle = commit("Freed 2", vec![reparent(2, BlockId::NULL)]);
649 let rev = host.ingest(&undo_cycle).expect("the host accepts it");
650 session
651 .apply_foreign(rev, &undo_cycle)
652 .expect("the delivery lands");
653 assert_eq!(
654 parent_of(&session, 1),
655 block_id(2),
656 "and reappears once the head makes it valid again",
657 );
658
659 let pending = session
660 .last_submission()
661 .expect("the edit stayed queued through both rebuilds")
662 .1
663 .clone();
664 let rev = host.ingest(&pending).expect("the host accepts it now");
665 session.committed(nonce, rev).expect("the ack lands");
666 assert_eq!(
667 *session
668 .confirmed()
669 .block(&block_id(1))
670 .expect("block 1 is present")
671 .as_ref()
672 .parent
673 .as_ref(),
674 block_id(2),
675 );
676 }
677
678 #[test]
679 fn a_refused_ingest_consumes_no_rev_and_leaves_the_log_alone() {
680 let mut host = Host::default();
681 let accepted = host
682 .ingest(&commit("Added a block", vec![block_create(1)]))
683 .expect("the host accepts it");
684
685 assert!(
686 host.ingest(&commit("Orphan pin", vec![orphan_pin(3)]))
687 .is_err(),
688 );
689 assert_eq!(host.rev(), accepted, "a refusal mints no rev");
690 assert_eq!(host.commits_after(Rev::ZERO).len(), 1, "and logs nothing");
691
692 let next = host
693 .ingest(&commit("Added another", vec![block_create(2)]))
694 .expect("the host accepts it");
695 assert_eq!(
696 next,
697 accepted.next(),
698 "the next commit takes the rev the refusal did not consume",
699 );
700 }
701
702 #[test]
703 fn commits_after_replays_the_tail_in_rev_order() {
704 let mut host = Host::default();
705 for byte in 1..=3 {
706 host.ingest(&commit("Added a block", vec![block_create(byte)]))
707 .expect("the host accepts it");
708 }
709
710 assert_eq!(host.commits_after(Rev::ZERO).len(), 3, "the whole log");
711 assert_eq!(
712 host.commits_after(Rev::new(2)).len(),
713 1,
714 "log[i] is Rev(i+1)"
715 );
716 assert!(host.commits_after(host.rev()).is_empty(), "caught up");
717 assert!(
718 host.commits_after(host.rev().next()).is_empty(),
719 "and a rev beyond the head does not panic",
720 );
721 }
722
723 fn assert_round_trip<R: RevKind>(start: &Document<R>, edit: &Commit) -> Commit {
726 let after = start.try_apply(edit).expect("the edit folds");
727 assert_ne!(
728 projection(&after),
729 projection(start),
730 "the edit must be observable or the round trip proves nothing",
731 );
732
733 let inverse = inverse_of(start, edit);
734 let back = after.try_apply(&inverse).expect("the inverse folds");
735 assert_eq!(projection(&back), projection(start));
736 inverse
737 }
738
739 #[test]
740 fn the_journal_inverts_a_commit_that_writes_one_register_twice() {
741 let start = Document::<Confirmed>::default()
742 .try_apply(&commit("Added a block", vec![block_create(1)]))
743 .expect("the fold succeeds");
744
745 let inverse = assert_round_trip(
748 &start,
749 &commit(
750 "Dragged it",
751 vec![resize(1, rect(5, 5)), resize(1, rect(9, 9))],
752 ),
753 );
754 assert_eq!(inverse.ops().len(), 2);
755 }
756
757 #[test]
758 fn the_journal_inverts_a_create_and_a_delete() {
759 let start = Document::<Confirmed>::default()
760 .try_apply(&commit("Added a block", vec![block_create(1)]))
761 .expect("the fold succeeds");
762
763 assert_round_trip(&start, &commit("Added another", vec![block_create(2)]));
764 assert_round_trip(
765 &start,
766 &commit(
767 "Deleted it",
768 vec![OpCodes::Block(block_id(1), Crud::Delete)],
769 ),
770 );
771 }
772
773 #[test]
779 fn the_inverse_of_a_lifecycle_pair_unwinds_in_reverse() {
780 let start = Document::<Confirmed>::default();
781 let edit = commit(
782 "Added and dropped a block",
783 vec![block_create(1), OpCodes::Block(block_id(1), Crud::Delete)],
784 );
785 let after = start.try_apply(&edit).expect("the edit folds");
786 assert!(
787 projection(&after).is_empty(),
788 "the block ends tombstoned, so it was never visible",
789 );
790
791 let inverse = inverse_of(&start, &edit);
792 assert!(
793 matches!(inverse.ops().last(), Some(OpCodes::Block(_, Crud::Delete))),
794 "the create's inverse must run last",
795 );
796 let back = after.try_apply(&inverse).expect("the inverse folds");
797 assert!(
798 back.block(&block_id(1)).is_none_or(|live| !live.is_alive()),
799 "undo must not resurrect what the commit only ever tombstoned",
800 );
801 }
802
803 #[test]
804 fn the_journal_skips_an_update_to_an_entity_the_commit_created() {
805 let start = Document::<Confirmed>::default();
806 let inverse = assert_round_trip(
807 &start,
808 &commit(
809 "Added and placed a block",
810 vec![block_create(1), resize(1, rect(5, 5))],
811 ),
812 );
813
814 assert_eq!(
815 inverse.ops().len(),
816 1,
817 "the update has no baseline to restore; the create's delete covers it",
818 );
819 assert!(matches!(
820 inverse.ops().first(),
821 Some(OpCodes::Block(_, Crud::Delete)),
822 ));
823 }
824
825 #[test]
826 fn the_journal_inverts_the_singleton() {
827 let start = Document::<Confirmed>::default()
828 .try_apply(&commit(
829 "Named it",
830 vec![OpCodes::Document(TitleBlockUpdate::Name("first".into()))],
831 ))
832 .expect("the fold succeeds");
833
834 let edit = commit(
835 "Renamed it",
836 vec![OpCodes::Document(TitleBlockUpdate::Name("second".into()))],
837 );
838 let after = start.try_apply(&edit).expect("the edit folds");
839 let back = after
840 .try_apply(&inverse_of(&start, &edit))
841 .expect("the inverse folds");
842
843 assert_eq!(after.title_block().name.as_ref().as_str(), "second");
844 assert_eq!(back.title_block().name.as_ref().as_str(), "first");
845 }
846
847 #[test]
848 fn undo_and_redo_travel_as_ordinary_commits() {
849 let (mut host, mut session) = one_block();
850 let placed = projection(session.confirmed());
851 edit(
852 &mut host,
853 &mut session,
854 commit("Moved it", vec![resize(1, rect(5, 5))]),
855 );
856 let moved = projection(session.confirmed());
857 assert_ne!(placed, moved);
858
859 session
860 .undo()
861 .expect("something to undo")
862 .expect("the inverse folds");
863 ack_last(&mut host, &mut session);
864 assert_eq!(projection(session.confirmed()), placed, "undo restored it");
865 assert!(session.can_redo());
866
867 session
868 .redo()
869 .expect("something to redo")
870 .expect("the inverse folds");
871 ack_last(&mut host, &mut session);
872 assert_eq!(projection(session.confirmed()), moved, "redo re-applied it");
873 assert!(session.can_undo());
874 assert!(!session.can_redo(), "the redo future is spent");
875 }
876
877 #[test]
878 fn a_fresh_edit_abandons_the_redo_future() {
879 let (mut host, mut session) = one_block();
880 edit(
881 &mut host,
882 &mut session,
883 commit("Moved it", vec![resize(1, rect(5, 5))]),
884 );
885 session
886 .undo()
887 .expect("something to undo")
888 .expect("the inverse folds");
889 ack_last(&mut host, &mut session);
890 assert!(session.can_redo());
891
892 edit(
893 &mut host,
894 &mut session,
895 commit("Moved it elsewhere", vec![resize(1, rect(7, 7))]),
896 );
897 assert!(!session.can_redo(), "a new edit forks the history");
898 }
899
900 #[test]
901 fn welcome_folds_a_log_into_a_session() {
902 let mut host = Host::default();
903 for byte in 1..=3 {
904 host.ingest(&commit("Added a block", vec![block_create(byte)]))
905 .expect("the host accepts it");
906 }
907
908 let session = ClientSession::welcome(host.commits_after(Rev::ZERO)).expect("the log folds");
909 assert_eq!(session.rev(), host.rev());
910 assert_eq!(
911 session.confirmed().content_hash(),
912 host.state().content_hash(),
913 "a replayed log reproduces the host's document exactly",
914 );
915 }
916}