1use std::any::Any;
8use std::cell::RefCell;
9use std::collections::HashMap;
10use std::rc::Rc;
11
12use tilo_actions::{Action as _, ActionContext, ActionError, ActionInvoker, ChangeKind};
13
14use super::live::Live;
15use super::record::{Held, RecordDoor};
16use super::section::{SectionDoor, SectionKind};
17use tilo_resource::{Invoke, Resource};
18use ui_core::LocalState;
19use ui_core::history_mode::HistoryOwner;
20use ui_core::record::RecordKind;
21use ui_core::record::StoredRecord;
22use ui_core::record::person::PersonRecords;
23
24pub struct PersonRecord<P: Resource> {
40 shared: Rc<Shared<P>>,
41}
42
43impl<P: Resource> Clone for PersonRecord<P> {
44 fn clone(&self) -> Self {
45 Self {
46 shared: Rc::clone(&self.shared),
47 }
48 }
49}
50
51struct Shared<P: Resource> {
52 record: StoredRecord,
53 owner: Option<&'static str>,
56 app_id: &'static str,
57 app_title: &'static str,
58 me: String,
59 live: Live<P>,
61 state: LocalState<Rc<P::State>>,
63 held: RefCell<Option<Rc<Held<P>>>>,
65}
66
67impl<P: Resource> Shared<P> {
68 fn refresh(&self) -> bool {
71 let mut moved = match self.record.read(|doc| self.live.sync(doc)) {
72 Some(Ok(moved)) => moved,
73 Some(Err(error)) => {
74 unreadable_person_record(P::KIND, &error);
75 false
76 }
77 None => false,
78 };
79 if !moved && self.live.unreadable().is_none() {
80 match self.record.read(|doc| self.live.reread(doc)) {
85 Some(Ok(reread)) => moved = reread,
86 Some(Err(error)) => unreadable_person_record(P::KIND, &error),
87 None => {}
88 }
89 }
90 let _ = self.live.take_change();
92 if moved {
93 self.state.set(self.live.get());
94 }
95 moved
96 }
97}
98
99impl<P: Resource> PersonRecord<P> {
100 #[must_use]
102 pub fn get(&self) -> Rc<P::State> {
103 self.shared.refresh();
104 self.shared.state.with(Rc::clone)
105 }
106
107 pub fn with<T>(&self, read: impl FnOnce(&P::State) -> T) -> T {
109 read(&self.get())
110 }
111
112 #[must_use]
116 pub fn unreadable(&self) -> Option<String> {
117 self.shared.refresh();
118 self.shared.live.unreadable().map(|why| why.reason)
119 }
120
121 #[must_use]
124 pub fn state(&self) -> LocalState<Rc<P::State>> {
125 self.shared.state.clone()
126 }
127
128 #[must_use]
130 pub fn revision(&self) -> u64 {
131 self.shared.refresh();
132 self.shared.live.revision()
133 }
134
135 pub fn apply(&self, action: &P::Action) -> Result<(), ActionError> {
139 self.apply_as(action, &ActionInvoker::actor(&self.shared.me))
140 }
141
142 pub(crate) fn apply_as(
145 &self,
146 action: &P::Action,
147 invoker: &ActionInvoker,
148 ) -> Result<(), ActionError> {
149 let shared = &self.shared;
150 if let Some(owner) = shared.owner {
151 return Err(ActionError::Refused(format!(
152 "the person's {} are edited in {owner}",
153 P::LABEL.to_lowercase()
154 )));
155 }
156 let encoded = tilo_resource::encode::<P>(action)?;
157 let change = action.change_kind();
158 let context = ActionContext::for_resource(P::KIND, P::KIND, P::LABEL)
159 .with_app(shared.app_id, shared.app_title)
160 .with_invoker(invoker.clone())
161 .with_change(change);
162 shared
163 .record
164 .edit(change == ChangeKind::Content, |doc| {
165 let writer =
166 ui_core::history_source::authors::device_actor(&shared.me, doc.get_actor());
167 tilo_actions::apply_local_with_context(doc, action, encoded, Some(&writer), context)
168 .map(|_| ())
169 .map_err(|error| error.to_string())
170 })
171 .map_err(ActionError::Refused)?;
172 shared.refresh();
173 Ok(())
174 }
175
176 pub fn invoke(&self, invoke: &Invoke<P>) -> Result<(), ActionError> {
178 let action = invoke
179 .build(tilo_resource::origin_now_ms())
180 .map_err(ActionError::Refused)?;
181 self.apply(&action)
182 }
183
184 #[must_use]
187 pub fn editable(&self) -> bool {
188 self.shared.owner.is_none()
189 }
190
191 pub fn undo(&self) -> bool {
193 let undone = self.shared.record.undo();
194 self.shared.refresh();
195 undone
196 }
197
198 pub fn redo(&self) -> bool {
200 let redone = self.shared.record.redo();
201 self.shared.refresh();
202 redone
203 }
204
205 #[must_use]
208 pub fn syncs(&self) -> bool {
209 self.shared.record.syncs()
210 }
211
212 #[must_use]
217 pub fn content(&self) -> Option<tilo_content::ContentStore> {
218 self.shared.record.content()
219 }
220
221 #[must_use]
224 pub fn record(&self) -> &StoredRecord {
225 &self.shared.record
226 }
227}
228
229type OpenKey = (&'static str, String);
231
232pub(crate) struct PersonState {
235 records: Option<PersonRecords>,
239 own: Option<PersonRecords>,
243 app_id: &'static str,
244 app_title: &'static str,
245 me: String,
246 earlier: Vec<String>,
249 opened: RefCell<HashMap<OpenKey, Rc<dyn Any>>>,
251 others: RefCell<HashMap<&'static str, PersonRecords>>,
253 owners: Rc<RefCell<Vec<HistoryOwner>>>,
254 refreshers: RefCell<Vec<Box<dyn Fn() -> bool>>>,
255 published: RefCell<Vec<Published>>,
258 unavailable: RefCell<HashMap<(String, &'static str), tilo_actions::ResourceSpec>>,
263}
264
265struct Published {
267 id: String,
268 label: String,
269 door: Rc<dyn RecordDoor>,
271 step: Step,
274}
275
276type Step = Box<dyn Fn(bool, &str) -> bool>;
278
279struct Opened {
282 shared: Rc<dyn Any>,
283 door: Rc<dyn RecordDoor>,
284 step: Step,
285}
286
287pub(crate) const HISTORY_TYPE: &str = "tilo.app.person-history";
289
290const MAX_STEPS: i64 = 50;
292
293#[derive(serde::Serialize, serde::Deserialize)]
295struct HistoryRequest {
296 record: String,
297 undo: bool,
298 steps: Option<i64>,
299}
300
301#[must_use]
303pub(crate) fn resource_id(kind: &str) -> String {
304 format!("person:{kind}")
305}
306
307#[derive(Clone, Copy)]
316pub struct PersonKind {
317 kind: &'static str,
318 report: fn() -> tilo_resource::Surface,
319 open: fn(&super::settings::ViewStorage),
320}
321
322impl PersonKind {
323 #[must_use]
326 pub const fn shared<P: tilo_resource::SharedRecord>() -> Self {
327 Self {
328 kind: P::KIND,
329 report: shared_surface::<P>,
330 open: open_shared::<P>,
331 }
332 }
333
334 #[must_use]
336 pub const fn kind(&self) -> &'static str {
337 self.kind
338 }
339
340 #[must_use]
343 pub fn surface(&self) -> tilo_resource::Surface {
344 (self.report)()
345 }
346
347 pub(crate) fn open(&self, storage: &super::settings::ViewStorage) {
349 (self.open)(storage);
350 }
351}
352
353fn shared_surface<P: tilo_resource::SharedRecord>() -> tilo_resource::Surface {
354 tilo_resource::Surface {
355 person: true,
356 ..P::surface()
357 }
358}
359
360fn open_shared<P: tilo_resource::SharedRecord>(storage: &super::settings::ViewStorage) {
361 let _ = storage.shared_person::<P>();
362}
363
364fn history_spec(record: &str, label: &str, undo: bool) -> tilo_actions::ActionSpec {
365 let mut spec = history_operation(record.to_owned(), label, undo);
366 spec.type_mut().contract.result = tilo_actions::ResultSpec::Json {
367 schema: serde_json::json!({
368 "type": "object",
369 "properties": { "steps": { "type": "integer" } },
370 }),
371 };
372 spec
373}
374
375fn history_operation(record: String, label: &str, undo: bool) -> tilo_actions::ActionSpec {
376 let (id, verb) = if undo {
377 ("undo", "Undo")
378 } else {
379 ("redo", "Redo")
380 };
381 tilo_actions::ActionSpec::typed_operation(
382 id,
383 verb,
384 verb,
385 tilo_actions::OperationKind::Effect,
386 HISTORY_TYPE,
387 vec![
388 tilo_actions::ActionParam::optional(
389 "steps",
390 tilo_actions::ParamType::Number,
391 "How many of your steps (default 1, at most 50)",
392 )
393 .nullable(),
394 ],
395 move |args| {
396 tilo_actions::EncodedAction::encode_operation(
397 HISTORY_TYPE,
398 &HistoryRequest {
399 record: record.clone(),
400 undo,
401 steps: args.integer("steps"),
402 },
403 )
404 .unwrap_or_else(|_| tilo_actions::EncodedAction {
405 type_id: tilo_actions::resource::INVALID_ARGUMENTS_TYPE.to_owned(),
406 payload: Vec::new(),
407 })
408 },
409 )
410 .of_kind(tilo_actions::ActionKind::Read)
411 .with_description(if undo {
412 format!("Take back your last change to your {label}.")
413 } else {
414 format!("Make again the change to your {label} you last took back.")
415 })
416}
417
418impl PersonState {
419 pub(crate) fn new(
420 records: PersonRecords,
421 own: Option<PersonRecords>,
422 app_id: &'static str,
423 app_title: &'static str,
424 ) -> Self {
425 let me = records.author().0.clone();
426 Self::named(Some(records), own, me, Vec::new(), app_id, app_title)
427 }
428
429 pub(crate) fn without_records(
435 me: String,
436 earlier: Vec<String>,
437 app_id: &'static str,
438 app_title: &'static str,
439 ) -> Self {
440 Self::named(None, None, me, earlier, app_id, app_title)
441 }
442
443 fn named(
444 records: Option<PersonRecords>,
445 own: Option<PersonRecords>,
446 me: String,
447 earlier: Vec<String>,
448 app_id: &'static str,
449 app_title: &'static str,
450 ) -> Self {
451 Self {
452 records,
453 own,
454 app_id,
455 app_title,
456 me,
457 earlier,
458 opened: RefCell::default(),
459 others: RefCell::default(),
460 owners: Rc::default(),
461 refreshers: RefCell::default(),
462 published: RefCell::default(),
463 unavailable: RefCell::default(),
464 }
465 }
466
467 pub(crate) fn open<P: Resource>(&self) -> Option<PersonRecord<P>> {
469 self.open_of::<P>(self.app_id, true, true, false)
470 }
471
472 pub(crate) fn open_shared<P: tilo_resource::SharedRecord>(&self) -> Option<PersonRecord<P>> {
475 self.open_of::<P>(P::OWNER, P::edited_by(self.app_id), true, true)
476 }
477
478 pub(crate) fn hold_shared<P: tilo_resource::SharedRecord>(&self) -> Option<PersonRecord<P>> {
482 self.open_of::<P>(P::OWNER, P::edited_by(self.app_id), false, true)
483 }
484
485 fn open_of<P: Resource>(
490 &self,
491 owner: &'static str,
492 edits: bool,
493 publish: bool,
494 shared: bool,
495 ) -> Option<PersonRecord<P>> {
496 let open = |record: StoredRecord| -> Result<Opened, String> {
497 let shared = self.shared_of::<P>(record, (!edits).then_some(owner))?;
498 let held = Rc::new(held::<P>(&shared, edits));
499 *shared.held.borrow_mut() = Some(Rc::clone(&held));
500 let weak = Rc::downgrade(&shared);
501 Ok(Opened {
502 shared: shared as Rc<dyn Any>,
503 door: held,
504 step: Box::new(move |undo, _| {
505 weak.upgrade().is_some_and(|shared| {
506 let record = PersonRecord { shared };
507 if undo { record.undo() } else { record.redo() }
508 })
509 }),
510 })
511 };
512 let shared = self.open_kind(
513 owner,
514 &RecordType {
515 kind: P::KIND,
516 label: P::LABEL,
517 unreadable: &|reason| {
518 tilo_resource::resource::unreadable_parts::<P>(
519 &resource_id(P::KIND),
520 P::LABEL,
521 reason,
522 )
523 },
524 },
525 &|doc| P::initialize(doc).map_err(|error| error.to_string()),
526 &open,
527 (publish, shared),
528 )?;
529 shared
530 .downcast::<Shared<P>>()
531 .ok()
532 .map(|shared| PersonRecord { shared })
533 }
534
535 fn open_kind(
542 &self,
543 owner: &'static str,
544 of: &RecordType<'_>,
545 initialize: &dyn Fn(&mut ui_core::automerge::AutoCommit) -> Result<(), String>,
546 open: &dyn Fn(StoredRecord) -> Result<Opened, String>,
547 (publish, shared): (bool, bool),
548 ) -> Option<Rc<dyn Any>> {
549 let (kind, label) = (of.kind, of.label);
550 let key = (owner, kind.to_owned());
551 if let Some(open) = self.opened.borrow().get(&key) {
552 return Some(Rc::clone(open));
553 }
554 let records = if shared {
555 self.records_of(owner)?
556 } else {
557 self.own.clone()?
558 };
559 let record = match open_record(&records, kind.to_owned(), kind, initialize) {
560 Ok(record) => record,
561 Err(reason) => return self.note_unavailable(owner, of, &reason, publish),
562 };
563 let Opened { shared, door, step } = match open(record) {
564 Ok(opened) => opened,
565 Err(reason) => return self.note_unavailable(owner, of, &reason, publish),
566 };
567 if owner != self.app_id || !publish {
568 self.opened.borrow_mut().insert(key, Rc::clone(&shared));
569 return Some(shared);
570 }
571 self.publish(
572 key,
573 Rc::clone(&shared),
574 Published {
575 id: resource_id(kind),
576 label: label.to_owned(),
577 door,
578 step,
579 },
580 );
581 Some(shared)
582 }
583
584 fn shared_of<P: Resource>(
590 &self,
591 record: StoredRecord,
592 owner: Option<&'static str>,
593 ) -> Result<Rc<Shared<P>>, String> {
594 let older = record
598 .read(tilo_resource::evolution::needs_migrate::<P>)
599 .ok_or_else(|| "the record cannot be read".to_owned())?;
600 if older
601 && let Err(error) = record.edit(false, |doc| {
602 tilo_resource::evolution::migrate::<P>(doc).map_err(|error| error.to_string())
603 })
604 {
605 return Err(format!(
606 "it could not be brought up to this app's storage version: {error}"
607 ));
608 }
609 let reader = record
610 .read(tilo_resource::Reader::<P>::read)
611 .ok_or_else(|| "the record cannot be read".to_owned())?
612 .map_err(|error| format!("it does not read: {error}"))?;
613 let live = Live::new(reader);
614 self.owners.borrow_mut().push(record.history_owner());
615 let shared = Rc::new(Shared::<P> {
616 record,
617 owner,
618 app_id: self.app_id,
619 app_title: self.app_title,
620 me: self.me.clone(),
621 state: LocalState::new(live.get()),
622 live,
623 held: RefCell::new(None),
624 });
625 let weak = Rc::downgrade(&shared);
626 self.refreshers.borrow_mut().push(Box::new(move || {
627 weak.upgrade().is_some_and(|shared| shared.refresh())
628 }));
629 Ok(shared)
630 }
631
632 pub(crate) fn open_section(&self, kind: Rc<SectionKind>) -> Rc<SectionDoor> {
635 let key = (self.app_id, kind.kind.to_owned());
636 if let Some(door) = self
637 .opened
638 .borrow()
639 .get(&key)
640 .and_then(|open| Rc::clone(open).downcast::<SectionDoor>().ok())
641 {
642 return door;
643 }
644 let door = Rc::new(SectionDoor::new(
645 kind,
646 self.me.clone(),
647 self.earlier.clone(),
648 ));
649 let stepping = Rc::downgrade(&door);
650 self.publish(
651 key,
652 Rc::clone(&door) as Rc<dyn Any>,
653 Published {
654 id: door.id().to_owned(),
655 label: door.label().to_owned(),
656 door: Rc::clone(&door) as Rc<dyn RecordDoor>,
657 step: Box::new(move |undo, by| {
658 stepping.upgrade().is_some_and(|door| door.step(undo, by))
659 }),
660 },
661 );
662 door
663 }
664
665 fn records_of(&self, owner: &'static str) -> Option<PersonRecords> {
668 if owner == self.app_id && !self.records.as_ref()?.shared_by_host() {
672 return self.records.clone();
673 }
674 let mut others = self.others.borrow_mut();
675 if let Some(records) = others.get(owner) {
676 return Some(records.clone());
677 }
678 let records = self
679 .records
680 .as_ref()?
681 .of_package(owner)
682 .map_err(|error| {
683 tracing::warn!(owner, %error, "the person's records of another app are unavailable");
684 })
685 .ok()?;
686 others.insert(owner, records.clone());
687 Some(records)
688 }
689
690 fn note_unavailable(
695 &self,
696 owner: &'static str,
697 of: &RecordType<'_>,
698 reason: &str,
699 publish: bool,
700 ) -> Option<Rc<dyn Any>> {
701 unreadable_person_record(of.kind, &reason);
702 if owner == self.app_id && publish {
706 let published = (of.unreadable)(reason);
707 self.unavailable
708 .borrow_mut()
709 .insert((owner.to_owned(), of.kind), published);
710 }
711 None
712 }
713
714 pub(crate) fn unavailable(&self) -> Vec<tilo_actions::ResourceSpec> {
717 self.unavailable.borrow().values().cloned().collect()
718 }
719
720 fn publish(&self, key: OpenKey, open: Rc<dyn Any>, published: Published) {
723 self.published.borrow_mut().push(published);
724 self.opened.borrow_mut().insert(key, open);
725 }
726
727 pub(crate) fn refresh(&self) -> bool {
729 self.refreshers
730 .borrow()
731 .iter()
732 .fold(false, |moved, refresh| refresh() | moved)
733 }
734
735 pub(crate) fn resources(&self) -> Vec<tilo_actions::ResourceSpec> {
739 self.published
740 .borrow()
741 .iter()
742 .map(|published| {
743 published
744 .door
745 .publish()
746 .with_action(history_spec(&published.id, &published.label, true))
747 .with_action(history_spec(&published.id, &published.label, false))
748 })
749 .collect()
750 }
751
752 pub(crate) fn doors(&self) -> Vec<Rc<dyn RecordDoor>> {
754 self.published
755 .borrow()
756 .iter()
757 .map(|published| Rc::clone(&published.door))
758 .collect()
759 }
760
761 pub(crate) fn me(&self) -> &str {
762 &self.me
763 }
764
765 pub(crate) fn answer(&self, payload: &[u8]) -> Result<tilo_actions::ActionOutput, String> {
767 let invoker = ui_core::invocation_scope::current_invoker()
768 .unwrap_or_else(|| ActionInvoker::actor(&self.me));
769 self.answer_for(payload, &invoker)
770 }
771
772 pub(crate) fn answer_for(
774 &self,
775 payload: &[u8],
776 invoker: &ActionInvoker,
777 ) -> Result<tilo_actions::ActionOutput, String> {
778 let request: HistoryRequest =
779 tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
780 let steps = request.steps.unwrap_or(1);
781 if !(1..=MAX_STEPS).contains(&steps) {
782 return Err(format!("steps is 1 to {MAX_STEPS}"));
783 }
784 if invoker.actor != self.me && invoker.on_behalf_of.as_deref() != Some(self.me.as_str()) {
785 return Err(format!(
786 "`{}` is {}'s own; only they change it",
787 request.record, self.me
788 ));
789 }
790 let published = self.published.borrow();
791 let record = published
792 .iter()
793 .find(|published| published.id == request.record)
794 .ok_or_else(|| format!("no record of yours is open as {}", request.record))?;
795 let mut taken = 0;
796 while taken < steps && (record.step)(request.undo, &invoker.actor) {
797 taken = taken.saturating_add(1);
798 }
799 if taken == 0 {
800 return Err(if request.undo {
801 "nothing of yours to undo".to_owned()
802 } else {
803 "nothing of yours to redo".to_owned()
804 });
805 }
806 Ok(tilo_actions::ActionOutput {
807 value: Some(serde_json::json!({ "steps": taken })),
808 resources: vec![record.id.clone()],
809 })
810 }
811
812 pub(crate) fn owners(&self) -> Vec<HistoryOwner> {
815 self.owners.borrow().clone()
816 }
817}
818
819fn open_record(
826 records: &PersonRecords,
827 kind: String,
828 of: &str,
829 initialize: &dyn Fn(&mut ui_core::automerge::AutoCommit) -> Result<(), String>,
830) -> Result<StoredRecord, String> {
831 records
832 .open_record(RecordKind::new(kind), initialize)
833 .map_err(|error| format!("{of}'s record cannot be opened: {error}"))
834}
835
836fn held<P: Resource>(shared: &Rc<Shared<P>>, edits: bool) -> Held<P> {
839 let following = Rc::downgrade(shared);
840 let applying = Rc::downgrade(shared);
841 Held::new(
842 resource_id(P::KIND),
843 format!("Your {}", P::LABEL.to_lowercase()),
844 edits,
845 shared.live.clone(),
846 move |_: &Live<P>| {
847 if let Some(shared) = following.upgrade() {
848 shared.refresh();
849 }
850 },
851 move |action: &P::Action, invoker: &ActionInvoker| {
852 let shared = applying
853 .upgrade()
854 .ok_or_else(|| ActionError::Refused("the record is closed".into()))?;
855 PersonRecord { shared }.apply_as(action, invoker)
856 },
857 )
858}
859
860impl<P: Resource> super::record::sealed::Hold<P> for PersonRecord<P> {
861 fn held(&self, _storage: super::record::sealed::Storage<'_>) -> Option<Rc<Held<P>>> {
862 self.shared.held.borrow().clone()
863 }
864
865 fn door(
866 &self,
867 _storage: super::record::sealed::Storage<'_>,
868 ) -> Option<super::record::sealed::Door> {
869 self.shared
870 .held
871 .borrow()
872 .clone()
873 .map(|held| super::record::sealed::Door(held as Rc<dyn RecordDoor>))
874 }
875}
876
877impl Drop for PersonState {
878 fn drop(&mut self) {
880 for records in self.records.iter().chain(&self.own) {
881 if let Err(error) = records.flush() {
882 tracing::warn!(%error, "the records beside the app's own could not be stored");
883 }
884 }
885 }
886}
887
888#[cold]
891#[inline(never)]
892fn unreadable_person_record(kind: &'static str, error: &dyn std::fmt::Display) {
893 tracing::warn!(kind, %error, "person record unreadable");
894}
895
896struct RecordType<'a> {
899 kind: &'static str,
900 label: &'static str,
901 unreadable: &'a dyn Fn(&str) -> tilo_actions::ResourceSpec,
903}