1use blockworx_doc::{
17 commit::Commit,
18 document::Document,
19 repo::Repo,
20 rev::Rev,
21 trail::{Direction, Trail},
22};
23use serde::{Deserialize, Serialize};
24
25use crate::Refusal;
26use crate::history::Journal;
27use crate::record::{Attribution, Digest};
28use crate::revs;
29use crate::stamp::Stamp;
30use crate::tags::Tags;
31use crate::worked::Worked;
32
33use crate::container::{Drained, ReadOnlyReason};
34use crate::handle::Store;
35use crate::storage::{Any, Name};
36
37#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
40pub enum Writability {
41 Writable,
42 ReadOnly,
43}
44
45#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
54pub enum Viewing {
55 Head,
56 Past(Rev),
57}
58
59impl Viewing {
60 pub fn writability(self) -> Writability {
64 match self {
65 Viewing::Head => Writability::Writable,
66 Viewing::Past(_) => Writability::ReadOnly,
67 }
68 }
69
70 pub fn stepped(self, head: Rev, step: TimeStep) -> Option<At> {
79 let Viewing::Past(at) = self else {
80 return None;
81 };
82 match step {
83 TimeStep::Back => at.prev().filter(|before| *before > Rev::ZERO).map(At::Rev),
84 TimeStep::Forward if at >= head => Some(At::Current),
85 TimeStep::Forward => Some(At::Rev(at.next())),
86 }
87 }
88}
89
90#[derive(Clone, Copy, PartialEq, Eq, Debug)]
93pub enum At {
94 Rev(Rev),
95 Current,
96}
97
98#[derive(Clone, Copy, PartialEq, Eq, Debug)]
100pub enum TimeStep {
101 Back,
102 Forward,
103}
104
105#[derive(Clone, Copy, PartialEq, Eq, Debug)]
110pub enum Saving {
111 Offered,
112 Withheld,
113}
114
115#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
119pub enum Attachment {
120 Attached,
121 Scratch,
123}
124
125#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
130pub enum Renaming {
131 Offered,
132 Withheld,
133}
134
135impl From<Saving> for Renaming {
136 fn from(saving: Saving) -> Self {
137 match saving {
138 Saving::Offered => Renaming::Offered,
139 Saving::Withheld => Renaming::Withheld,
140 }
141 }
142}
143
144#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
153pub struct DocumentNonce(u64);
154
155impl DocumentNonce {
156 pub fn mint() -> Self {
165 use std::hash::{BuildHasher as _, Hasher as _, RandomState};
166 use std::sync::atomic::{AtomicU64, Ordering};
167 static OPENED: AtomicU64 = AtomicU64::new(0);
168 let mut hasher = RandomState::new().build_hasher();
169 hasher.write_u64(OPENED.fetch_add(1, Ordering::Relaxed));
170 Self(hasher.finish())
171 }
172}
173
174#[derive(Clone, Copy)]
177pub(crate) struct Stepping<'a> {
178 pub edit: Rev,
179 pub direction: Direction,
180 pub label: &'a str,
181}
182
183pub(crate) fn stepped(
195 repo: &mut Repo,
196 trail: &mut Trail,
197 step: Stepping<'_>,
198 at: impl FnOnce(Rev) -> Result<Document, Refusal>,
199) -> Result<Rev, Refusal> {
200 let Stepping {
201 edit,
202 direction,
203 label,
204 } = step;
205 let entry = trail.stepping(edit, direction)?;
206 let target = at(entry.restores)?;
207 Ok(repo.restore(
208 trail,
209 blockworx_doc::repo::Restoring {
210 entry,
211 direction,
212 target,
213 label,
214 },
215 ))
216}
217
218#[derive(Default)]
226pub struct Shelf {
227 revs: revs::Memory,
228 payloads: crate::assets::Held,
229 worked: std::collections::BTreeMap<Rev, Worked>,
234}
235
236impl Shelf {
237 fn keep(&mut self, at: Rev, document: &Document, worked: Worked) {
244 if let Err(why) = revs::write(&mut self.revs, at, document, &mut self.payloads) {
245 tracing::error!("rev {} was not kept: {why}", at.get());
246 }
247 self.worked.insert(at, worked);
248 }
249
250 fn worked_at(&self, at: Rev) -> Option<Worked> {
251 self.worked.get(&at).cloned()
252 }
253
254 fn at(&self, at: Rev) -> Result<Document, Refusal> {
259 let unreachable = |why: &dyn std::fmt::Display| Refusal::Unreachable {
260 at,
261 why: why.to_string(),
262 };
263 let document = revs::read(&self.revs, at).map_err(|why| unreachable(&why))?;
264 revs::attached(document, &self.payloads).map_err(|why| unreachable(&why))
265 }
266
267 fn stamp(&self, at: Rev) -> Digest {
270 revs::stamp(&self.revs, at).unwrap_or_else(|_| Digest::of(&[]))
271 }
272}
273
274fn label_of(repo: &Repo, at: Rev) -> Option<String> {
277 let ndx = usize::try_from(at.get().checked_sub(1)?).ok()?;
278 repo.log().get(ndx).map(|commit| commit.label().to_owned())
279}
280
281pub enum Doc {
285 Scratch {
289 repo: Box<Repo>,
290 trail: Trail,
294 tags: Tags,
295 revs: Box<Shelf>,
299 session: DocumentNonce,
300 },
301 Attached {
306 store: Box<Store<Any>>,
307 session: DocumentNonce,
308 },
309}
310
311impl Default for Doc {
312 fn default() -> Self {
313 Doc::scratch(Repo::default())
314 }
315}
316
317impl Doc {
318 pub fn scratch(repo: Repo) -> Self {
319 let mut trail = Trail::default();
320 trail.seeded(repo.rev());
323 let mut revs = Shelf::default();
327 let mut folding = Repo::default();
328 for commit in repo.log() {
329 let worked = Worked::of(folding.document(), commit);
330 match folding.fold_one(commit.clone()) {
331 Ok(document) => revs.keep(document.rev(), document, worked),
332 Err(why) => tracing::error!("a session's own past will not fold: {why}"),
333 }
334 }
335 revs.keep(repo.rev(), repo.document(), Worked::nothing());
336 Doc::Scratch {
337 repo: Box::new(repo),
338 trail,
339 tags: Tags::default(),
340 revs: Box::new(revs),
341 session: DocumentNonce::mint(),
342 }
343 }
344
345 pub fn attached(store: Store<Any>) -> Self {
346 Doc::Attached {
347 store: Box::new(store),
348 session: DocumentNonce::mint(),
349 }
350 }
351
352 pub fn session(&self) -> DocumentNonce {
356 match self {
357 Doc::Scratch { session, .. } | Doc::Attached { session, .. } => *session,
358 }
359 }
360
361 pub fn repo(&self) -> &Repo {
362 match self {
363 Doc::Scratch { repo, .. } => repo,
364 Doc::Attached { store, .. } => store.repo(),
365 }
366 }
367
368 pub fn trail(&self) -> &Trail {
371 match self {
372 Doc::Scratch { trail, .. } => trail,
373 Doc::Attached { store, .. } => store.trail(),
374 }
375 }
376
377 pub fn tags(&self) -> &Tags {
379 match self {
380 Doc::Scratch { tags, .. } => tags,
381 Doc::Attached { store, .. } => store.tags(),
382 }
383 }
384
385 pub fn journal(&self) -> Journal<'_> {
388 match self {
389 Doc::Scratch { repo, .. } => Journal::Session(repo.log()),
390 Doc::Attached { store, .. } => Journal::Recorded(store.rows()),
391 }
392 }
393
394 pub fn worked_at(&self, at: Rev) -> Option<Worked> {
401 match self {
402 Doc::Scratch { revs, .. } => revs.worked_at(at),
403 Doc::Attached { store, .. } => store.framing(at).map(Worked::recorded),
404 }
405 }
406
407 pub fn camera_at(&self, at: Rev) -> Option<crate::record::Camera> {
411 match self {
412 Doc::Scratch { .. } => None,
413 Doc::Attached { store, .. } => store.framing(at).map(|row| row.camera),
414 }
415 }
416
417 pub fn label_at(&self, at: Rev) -> Option<&str> {
420 match self {
421 Doc::Scratch { repo, .. } => {
422 let ndx = usize::try_from(at.get().checked_sub(1)?).ok()?;
423 repo.log().get(ndx).map(Commit::label)
424 }
425 Doc::Attached { store, .. } => store.row(at).map(|row| row.label.as_str()),
426 }
427 }
428
429 pub fn document_at(&self, at: Rev) -> Result<Document, Refusal> {
435 match self {
436 Doc::Scratch { revs, .. } => revs.at(at),
437 Doc::Attached { store, .. } => store.document_at(at),
438 }
439 }
440
441 pub fn stamp_at(&self, at: Rev) -> Stamp {
444 Stamp::at(at, self.state_at(at))
445 }
446
447 fn state_at(&self, at: Rev) -> Digest {
448 match self {
449 Doc::Scratch { revs, .. } => revs.stamp(at),
450 Doc::Attached { store, .. } => store
451 .row(at)
452 .map_or_else(|| Digest::of(&[]), |row| row.hash),
453 }
454 }
455
456 pub fn document(&self) -> &Document {
457 self.repo().document()
458 }
459
460 pub fn submit<'a>(
464 &mut self,
465 commit: Commit,
466 by: impl Into<Attribution<'a>>,
467 ) -> Result<Rev, Refusal> {
468 match self {
469 Doc::Scratch {
470 repo, trail, revs, ..
471 } => {
472 let worked = Worked::of(repo.document(), &commit);
475 let rev = repo.submit(commit, trail)?;
476 revs.keep(rev, repo.document(), worked);
477 Ok(rev)
478 }
479 Doc::Attached { store, .. } => store.submit_edit(commit, by),
480 }
481 }
482
483 pub fn tag<'a>(
490 &mut self,
491 rev: Rev,
492 name: &str,
493 how: crate::tags::Tagging,
494 by: impl Into<Attribution<'a>>,
495 ) -> Result<(), Refusal> {
496 match self {
497 Doc::Scratch { repo, tags, .. } => {
498 if rev == Rev::ZERO || rev > repo.rev() {
499 return Err(Refusal::NoSuchRev(rev));
500 }
501 tags.apply(rev, name, how);
502 Ok(())
503 }
504 Doc::Attached { store, .. } => store.tag(rev, name, how, by),
505 }
506 }
507
508 pub fn undo<'a>(&mut self, edit: Rev, by: impl Into<Attribution<'a>>) -> Result<Rev, Refusal> {
512 self.step(edit, Direction::Undo, by)
513 }
514
515 pub fn redo<'a>(&mut self, edit: Rev, by: impl Into<Attribution<'a>>) -> Result<Rev, Refusal> {
518 self.step(edit, Direction::Redo, by)
519 }
520
521 fn step<'a>(
526 &mut self,
527 edit: Rev,
528 direction: Direction,
529 by: impl Into<Attribution<'a>>,
530 ) -> Result<Rev, Refusal> {
531 match self {
532 Doc::Scratch {
533 repo, trail, revs, ..
534 } => {
535 let entry = trail.stepping(edit, direction)?;
537 let label = label_of(repo, entry.rev).ok_or(Refusal::NoSuchRev(edit))?;
538 let moved = revs.worked_at(entry.rev).unwrap_or_else(Worked::nothing);
539 let at = stepped(
540 repo,
541 trail,
542 Stepping {
543 edit,
544 direction,
545 label: &label,
546 },
547 |restores| revs.at(restores),
548 )?;
549 revs.keep(at, repo.document(), moved);
550 Ok(at)
551 }
552 Doc::Attached { store, .. } => match direction {
553 Direction::Undo => store.undo(edit, by),
554 Direction::Redo => store.redo(edit, by),
555 },
556 }
557 }
558
559 pub fn writability(&self) -> Writability {
562 match self {
563 Doc::Scratch { .. } => Writability::Writable,
564 Doc::Attached { store, .. } => match store.read_only_reason() {
565 Some(_) => Writability::ReadOnly,
566 None => Writability::Writable,
567 },
568 }
569 }
570
571 pub fn read_only_reason(&self) -> Option<&ReadOnlyReason> {
573 match self {
574 Doc::Scratch { .. } => None,
575 Doc::Attached { store, .. } => store.read_only_reason(),
576 }
577 }
578
579 pub fn attachment(&self) -> Attachment {
582 match self {
583 Doc::Scratch { .. } => Attachment::Scratch,
584 Doc::Attached { .. } => Attachment::Attached,
585 }
586 }
587
588 pub fn saving(&self) -> Saving {
589 match self {
590 Doc::Scratch { .. } => Saving::Withheld,
591 Doc::Attached { store, .. } => match store.read_only_reason() {
592 Some(_) => Saving::Withheld,
593 None => Saving::Offered,
594 },
595 }
596 }
597
598 pub fn renaming(&self) -> Renaming {
600 self.saving().into()
601 }
602
603 pub fn rename(&mut self, to: &Name) -> Result<(), Refusal> {
610 match self {
611 Doc::Scratch { .. } => Err(Refusal::Detached),
612 Doc::Attached { store, .. } => store.rename(to),
613 }
614 }
615
616 pub async fn renaming_to(&mut self, to: &Name) -> Result<(), Refusal> {
622 match self {
623 Doc::Scratch { .. } => Err(Refusal::Detached),
624 Doc::Attached { store, .. } => store.renaming_to(to).await,
625 }
626 }
627
628 pub fn pending(&self) -> usize {
632 match self {
633 Doc::Scratch { .. } => 0,
634 Doc::Attached { store, .. } => store.pending(),
635 }
636 }
637
638 pub async fn drain(&mut self) -> std::io::Result<Drained> {
644 match self {
645 Doc::Scratch { .. } => Ok(Drained::NOTHING),
646 Doc::Attached { store, .. } => store.drain().await,
647 }
648 }
649
650 pub fn container_name(&self) -> Option<Name> {
653 match self {
654 Doc::Scratch { .. } => None,
655 Doc::Attached { store, .. } => Some(store.name()),
656 }
657 }
658
659 pub fn container_path(&self) -> Option<&std::path::Path> {
664 match self {
665 Doc::Scratch { .. } => None,
666 Doc::Attached { store, .. } => store.path(),
667 }
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use crate::record::Identity;
675 use blockworx_doc::fixtures::rev;
676
677 fn author() -> Identity {
678 Identity::new("ada")
679 }
680
681 #[test]
684 fn stepping_walks_the_log_and_falls_off_its_end_into_the_present() {
685 let head = rev(3);
686 let step = |at: Viewing, dir| at.stepped(head, dir);
687
688 assert_eq!(step(Viewing::Head, TimeStep::Back), None);
689 assert_eq!(step(Viewing::Head, TimeStep::Forward), None);
690 assert_eq!(step(Viewing::Past(rev(1)), TimeStep::Back), None);
691 assert_eq!(
692 step(Viewing::Past(rev(2)), TimeStep::Back),
693 Some(At::Rev(rev(1))),
694 );
695 assert_eq!(
696 step(Viewing::Past(rev(2)), TimeStep::Forward),
697 Some(At::Rev(rev(3))),
698 );
699 assert_eq!(
700 step(Viewing::Past(rev(3)), TimeStep::Forward),
701 Some(At::Current),
702 );
703 assert_eq!(
704 step(Viewing::Past(rev(3)), TimeStep::Back),
705 Some(At::Rev(rev(2))),
706 "the newest rev still steps back",
707 );
708 }
709
710 #[test]
712 fn tagging_a_scratch_rev_moves_neither_the_log_nor_the_trail() {
713 let mut doc = Doc::default();
714 let at = doc.submit(one_block(), &author()).expect("the edit lands");
715 let depth = doc.trail().undo_depth();
716
717 doc.tag(at, "Initial Draft", crate::tags::Tagging::Added, &author())
718 .expect("it tags");
719 assert_eq!(doc.tags().of(at), ["Initial Draft"]);
720 assert_eq!(doc.repo().rev(), at, "tagging spent a rev");
721 assert_eq!(doc.trail().undo_depth(), depth, "tagging moved the trail");
722
723 doc.tag(
724 at,
725 "Initial Draft",
726 crate::tags::Tagging::Removed,
727 &author(),
728 )
729 .expect("and untags");
730 assert!(doc.tags().is_empty());
731 assert!(matches!(
732 doc.tag(rev(9), "Nowhere", crate::tags::Tagging::Added, &author()),
733 Err(Refusal::NoSuchRev(_)),
734 ));
735 }
736
737 fn one_block() -> Commit {
738 Commit::new(
739 "Added a block".to_owned(),
740 vec![crate::fixture::block_create(1, "Adder")],
741 )
742 }
743
744 #[test]
747 fn a_scratch_document_submits_undoes_and_redoes() {
748 let mut doc = Doc::default();
749 assert_eq!(doc.writability(), Writability::Writable);
750
751 let edit = doc.submit(one_block(), &author()).expect("the edit lands");
752 let undone = doc.undo(edit, &author()).expect("and undoes");
753 doc.redo(undone, &author()).expect("and redoes");
754 assert_eq!(doc.repo().log().len(), 3, "an undo is a forward commit");
755 }
756
757 #[test]
761 fn a_scratch_session_steps_through_its_own_prefix_folds() {
762 use blockworx_doc::repo::Repo;
763
764 let mut doc = Doc::default();
765 let mut revs = Vec::new();
766 for n in 1..=3 {
767 revs.push(
768 doc.submit(
769 Commit::new(
770 format!("Added block {n}"),
771 vec![crate::fixture::block_create(n, &format!("b{n}"))],
772 ),
773 &author(),
774 )
775 .expect("the edit lands"),
776 );
777 }
778 let folded = |at: Rev| {
779 Repo::folding(&doc.repo().log()[..at.get() as usize])
780 .expect("the prefix folds")
781 .document()
782 .clone()
783 };
784 let at_three = folded(revs[2]);
785 let at_two = folded(revs[1]);
786 assert_ne!(at_two, at_three, "precondition: the last edit moved it");
787
788 let undone = doc.undo(revs[2], &author()).expect("the undo lands");
789 assert_eq!(doc.document(), &at_two, "the undo did not reach rev 2");
790 assert_eq!(doc.trail().standing(), revs[1]);
791
792 doc.undo(revs[1], &author()).expect("the second undo lands");
793 assert_eq!(doc.trail().standing(), revs[0]);
794
795 doc.redo(
796 doc.trail().next_redo().expect("a step to put back"),
797 &author(),
798 )
799 .expect("the redo lands");
800 doc.redo(undone, &author()).expect("and the second redo");
801 assert_eq!(
802 doc.document(),
803 &at_three,
804 "the walk back up did not return the document it started from",
805 );
806 assert_eq!(doc.trail().standing(), revs[2]);
807 }
808
809 #[test]
812 fn a_step_the_trail_does_not_hold_is_refused_as_a_step() {
813 let mut doc = Doc::default();
814 assert!(matches!(
815 doc.undo(blockworx_doc::fixtures::rev(1), &author()),
816 Err(Refusal::Step(blockworx_doc::trail::UndoRefusal::Spent)),
817 ));
818 }
819
820 #[test]
821 fn an_attached_document_writes_through_the_container() {
822 use crate::handle::Clock;
823
824 let dir = crate::fixture::dir("doc-attached");
825 let root = dir.join("doc.bwx");
826 let store = Store::create(Any::new(crate::storage::Native::at(&root)), Clock::System)
827 .expect("the container");
828 let mut doc = Doc::attached(store);
829 assert_eq!(doc.writability(), Writability::Writable);
830
831 doc.submit(one_block(), &author()).expect("the edit lands");
832 assert_eq!(
833 std::fs::read_to_string(root.join(crate::container::MANIFEST.as_str()))
834 .expect("the log")
835 .lines()
836 .count(),
837 1,
838 "an edit through the document handle did not reach the log",
839 );
840 }
841
842 #[test]
845 fn a_locked_container_is_read_only_and_refuses_writes() {
846 use crate::handle::Clock;
847 use crate::storage::Native;
848
849 let dir = crate::fixture::dir("doc-locked");
850 let root = dir.join("doc.bwx");
851 let _held = Store::create(Native::at(&root), Clock::System).expect("the first session");
852
853 let mut doc = Doc::attached(
854 Store::open(Any::new(Native::at(&root)), Clock::System).expect("the second"),
855 );
856 assert_eq!(doc.writability(), Writability::ReadOnly);
857 assert!(matches!(
858 doc.submit(one_block(), &author()),
859 Err(Refusal::ReadOnly),
860 ));
861 assert_eq!(doc.renaming(), Renaming::Withheld);
863 assert!(matches!(
864 doc.rename(&Name::of_document("renamed").expect("a name")),
865 Err(Refusal::ReadOnly),
866 ));
867 assert!(root.exists(), "the refused rename moved it anyway");
868 }
869
870 #[test]
873 fn a_scratch_session_has_no_container_to_rename() {
874 let mut doc = Doc::default();
875 assert_eq!(doc.renaming(), Renaming::Withheld);
876 assert_eq!(doc.container_name(), None);
877 assert!(matches!(
878 doc.rename(&Name::of_document("nowhere").expect("a name")),
879 Err(Refusal::Detached),
880 ));
881 }
882}