1use std::any::Any;
49use std::cell::{Cell, RefCell};
50use std::collections::BTreeMap;
51use std::rc::Rc;
52
53use tilo_actions::{ActionContext, ActionError, ActionInvoker, OperationKind};
54use tilo_resource::{Invoke, Reader, Resource};
55use ui_core::permissions::{LivePermissions, PermissionScope, TreePath};
56use ui_core::record::{RecordCheckout, RecordId, RecordKind, RecordOpenError, RecordStore};
57
58use super::dispatch::Dispatcher;
59use super::history::{PART_EDIT_TYPE, PartEdit};
60use super::live::Live;
61use super::record::RecordDoor;
62
63pub(crate) const CHAIN_ACTION_TYPE: &str = "tilo.app.chain-action";
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum PartArrival {
71 Arriving,
73 Unavailable,
77}
78
79pub trait Chained: Resource {
82 type Base: Resource;
85
86 fn chain() -> RecordId;
88
89 fn chain_beside(base: &RecordId) -> RecordId {
94 let _ = base;
95 Self::chain()
96 }
97
98 fn part_for(action: &Self::Action, parts: &[(u64, Rc<Self::State>)]) -> Result<u64, String>;
102
103 fn prepare(
108 action: &mut Self::Action,
109 base: &<Self::Base as Resource>::State,
110 ) -> Result<(), String> {
111 let _ = (action, base);
112 Ok(())
113 }
114
115 fn carry(previous: u64, part: &Self::State) -> Option<Invoke<Self>> {
121 let _ = (previous, part);
122 None
123 }
124
125 fn place(action: &Self::Action, base: &<Self::Base as Resource>::State) -> Option<String> {
132 let _ = (action, base);
133 None
134 }
135}
136
137enum Opened {
139 Held,
141 Started,
143 Asked,
146}
147
148struct Unopened {
150 reason: String,
151 retry: bool,
156}
157
158impl From<RecordOpenError> for Unopened {
159 fn from(error: RecordOpenError) -> Self {
160 Self {
161 retry: error.writer_busy(),
162 reason: error.to_string(),
163 }
164 }
165}
166
167impl From<String> for Unopened {
168 fn from(reason: String) -> Self {
169 Self {
170 reason,
171 retry: false,
172 }
173 }
174}
175
176#[derive(Clone)]
179pub(crate) struct WorkspaceRecords {
180 pub(crate) store: RecordStore,
181 pub(crate) me: String,
182 pub(crate) permissions: Option<LivePermissions>,
183 pub(crate) app_id: &'static str,
184 pub(crate) app_title: &'static str,
185}
186
187type Base<P> = Rc<dyn Fn() -> Rc<<<P as Chained>::Base as Resource>::State>>;
189
190pub(crate) struct ChainState<P: Chained> {
193 workspace: WorkspaceRecords,
194 base: Base<P>,
195 chain: RecordId,
197 id: String,
198 kind: RecordKind,
199 parts: RefCell<BTreeMap<u64, Live<P>>>,
200 waiting: RefCell<BTreeMap<u64, PartArrival>>,
204 empty: Rc<P::State>,
207 revision: Cell<u64>,
208 looked: Cell<Option<u64>>,
211 published: RefCell<Option<((u64, u64), tilo_actions::ResourceSpec)>>,
212}
213
214impl<P: Chained> ChainState<P> {
215 pub(crate) fn open(workspace: WorkspaceRecords, base: Base<P>) -> Option<Self> {
217 Self::open_at(workspace, base, P::chain())
218 }
219
220 pub(crate) fn open_at(
224 workspace: WorkspaceRecords,
225 base: Base<P>,
226 chain: RecordId,
227 ) -> Option<Self> {
228 let mut doc = ui_core::automerge::AutoCommit::new();
229 P::initialize(&mut doc)
230 .map_err(|error| tracing::warn!(kind = P::KIND, %error, "chain part has no genesis"))
231 .ok()?;
232 let empty = Reader::<P>::read(&doc)
233 .map_err(|error| tracing::warn!(kind = P::KIND, %error, "chain part unreadable"))
234 .ok()?
235 .state()
236 .clone();
237 let id = ui_core::record::chain_resource_id(&chain);
238 let state = Self {
239 workspace,
240 base,
241 chain,
242 id,
243 kind: RecordKind::new(P::KIND),
244 parts: RefCell::default(),
245 waiting: RefCell::default(),
246 empty,
247 revision: Cell::new(0),
248 looked: Cell::new(None),
249 published: RefCell::new(None),
250 };
251 state.look();
252 Some(state)
253 }
254
255 pub(crate) fn id(&self) -> &str {
256 &self.id
257 }
258
259 fn checkout(&self, index: u64) -> RecordCheckout {
260 RecordCheckout::new(self.chain.part(index), self.kind.clone())
261 }
262
263 pub(crate) fn parts(&self) -> Vec<(u64, Rc<P::State>)> {
265 self.refresh();
266 self.parts
267 .borrow()
268 .iter()
269 .map(|(index, live)| (*index, live.get()))
270 .collect()
271 }
272
273 pub(crate) fn head(&self) -> Option<u64> {
274 self.refresh();
275 self.parts.borrow().keys().next_back().copied()
276 }
277
278 pub(crate) fn revision(&self) -> u64 {
279 self.refresh();
280 self.revision.get()
281 }
282
283 fn moved(&self) {
284 self.revision.set(self.revision.get().wrapping_add(1));
285 }
286
287 pub(crate) fn refresh(&self) -> bool {
290 let mut moved = false;
291 for (index, live) in self.parts.borrow().iter() {
292 match self
293 .workspace
294 .store
295 .read(&self.chain.part(*index), |doc| live.sync(doc))
296 {
297 Some(Ok(true)) => {
298 self.waiting.borrow_mut().remove(index);
300 moved = true;
301 }
302 Some(Err(error)) => {
303 tracing::warn!(kind = P::KIND, part = index, %error, "chain part unreadable");
304 }
305 _ => {}
306 }
307 let _ = live.take_change();
308 }
309 let opened: Vec<u64> = {
312 let parts = self.parts.borrow();
313 self.workspace
314 .store
315 .open_parts(&self.chain)
316 .into_iter()
317 .filter(|index| !parts.contains_key(index))
318 .collect()
319 };
320 for index in opened {
321 moved |= self.follow(index).is_ok();
322 }
323 if self.due() {
324 moved |= self.look();
325 moved |= self.follow_waiting();
326 }
327 if moved {
328 self.moved();
329 }
330 moved
331 }
332
333 fn due(&self) -> bool {
342 self.workspace.store.takes_writes()
343 && self.looked.get() != Some(self.workspace.store.kept_revision())
344 }
345
346 fn follow_waiting(&self) -> bool {
349 let waiting: Vec<(u64, PartArrival)> = self
350 .waiting
351 .borrow()
352 .iter()
353 .map(|(index, arrival)| (*index, *arrival))
354 .collect();
355 let mut moved = false;
356 for (index, arrival) in waiting {
357 let held = self.parts.borrow().contains_key(&index);
358 if !held {
359 match self.open_or_start(index) {
360 Ok(Opened::Held | Opened::Started) => {
362 self.waiting.borrow_mut().remove(&index);
363 moved = true;
364 continue;
365 }
366 Ok(Opened::Asked) => {}
367 Err(Unopened {
368 reason,
369 retry: true,
370 }) => {
371 tracing::warn!(kind = P::KIND, part = index, %reason, "earlier part not opened yet");
374 self.looked.set(None);
375 }
376 Err(Unopened { reason, .. }) => {
377 tracing::warn!(kind = P::KIND, part = index, %reason, "earlier part not opened");
378 }
379 }
380 }
381 let now = if self
382 .workspace
383 .store
384 .workspace_answered(&self.chain.part(index))
385 {
386 PartArrival::Unavailable
387 } else {
388 PartArrival::Arriving
389 };
390 if now != arrival {
391 self.waiting.borrow_mut().insert(index, now);
392 moved = true;
393 }
394 }
395 moved
396 }
397
398 fn look(&self) -> bool {
401 let looked = self.workspace.store.kept_revision();
402 let head = self.parts.borrow().keys().next_back().copied();
403 let kept = self.workspace.store.held_parts(&self.chain).last().copied();
406 let started = self
409 .workspace
410 .store
411 .announced_parts(&self.chain)
412 .last()
413 .copied();
414 let Some(newest) = kept.max(started) else {
415 self.looked.set(Some(looked));
416 return false;
417 };
418 if head.is_some_and(|head| newest <= head) {
419 self.looked.set(Some(looked));
420 return false;
421 }
422 let opened = if kept.is_some_and(|kept| kept >= newest) {
423 self.hold_kept(newest)
424 } else {
425 self.open_or_start(newest).and_then(|opened| match opened {
429 Opened::Held => Ok(()),
430 Opened::Started => {
431 self.waiting
432 .borrow_mut()
433 .insert(newest, PartArrival::Arriving);
434 Ok(())
435 }
436 Opened::Asked => Err(Unopened {
437 reason: "the part is on its way from the workspace".into(),
438 retry: false,
439 }),
440 })
441 };
442 match opened {
443 Ok(()) => {
444 self.looked.set(Some(looked));
445 true
446 }
447 Err(Unopened { reason, retry }) => {
448 tracing::warn!(kind = P::KIND, part = newest, %reason, "chain head not opened");
454 self.looked.set(if retry { None } else { Some(looked) });
455 false
456 }
457 }
458 }
459
460 fn refused(&self, error: Unopened) -> ActionError {
464 if error.retry {
465 self.looked.set(None);
466 }
467 ActionError::Refused(error.reason)
468 }
469
470 fn hold_kept(&self, index: u64) -> Result<(), Unopened> {
473 if self.parts.borrow().contains_key(&index) {
474 return Ok(());
475 }
476 let checkout = self.checkout(index);
477 if !self.workspace.store.is_ready(&checkout.id) {
478 self.workspace
479 .store
480 .try_open_record(&checkout)
481 .map_err(Unopened::from)?;
482 }
483 self.follow(index).map_err(|reason| Unopened {
484 reason,
485 retry: false,
486 })
487 }
488
489 pub(crate) fn hold(&self, index: u64) -> Result<(), String> {
498 match self.open_or_start(index) {
499 Ok(Opened::Held) => {}
500 Ok(Opened::Started | Opened::Asked) => {
501 self.waiting
502 .borrow_mut()
503 .insert(index, PartArrival::Arriving);
504 self.looked.set(None);
507 self.moved();
508 }
509 Err(Unopened {
510 retry: true,
511 reason,
512 }) => {
513 tracing::warn!(kind = P::KIND, part = index, %reason, "part waits for another writer");
517 self.waiting
518 .borrow_mut()
519 .insert(index, PartArrival::Arriving);
520 self.looked.set(None);
521 self.moved();
522 }
523 Err(Unopened { reason, .. }) => return Err(reason),
524 }
525 Ok(())
526 }
527
528 pub(crate) fn waiting(&self, index: u64) -> Option<PartArrival> {
531 self.waiting.borrow().get(&index).copied()
532 }
533
534 pub(crate) fn waited(&self) -> Vec<(u64, PartArrival)> {
536 self.waiting
537 .borrow()
538 .iter()
539 .map(|(index, arrival)| (*index, *arrival))
540 .collect()
541 }
542
543 fn start(&self, index: u64) -> Result<(), String> {
545 match self.open_or_start(index) {
546 Ok(Opened::Held | Opened::Started) => Ok(()),
547 Ok(Opened::Asked) => Err("the part is on its way from the workspace".into()),
548 Err(error) => {
549 if error.retry {
550 self.looked.set(None);
553 }
554 Err(error.reason)
555 }
556 }
557 }
558
559 fn open_or_start(&self, index: u64) -> Result<Opened, Unopened> {
562 if self.parts.borrow().contains_key(&index) {
563 return Ok(Opened::Held);
564 }
565 let checkout = self.checkout(index);
566 let mut opened = Opened::Held;
567 if !self.workspace.store.is_ready(&checkout.id) {
568 match self.workspace.store.try_open_record(&checkout) {
569 Ok(_) => {}
570 Err(RecordOpenError::Arriving) => return Ok(Opened::Asked),
571 Err(RecordOpenError::MissingRecord | RecordOpenError::IdentityMismatch) => {
575 self.create(&checkout)?;
576 opened = Opened::Started;
577 }
578 Err(error) => return Err(error.into()),
579 }
580 }
581 self.follow(index).map_err(|reason| Unopened {
582 reason,
583 retry: false,
584 })?;
585 Ok(opened)
586 }
587
588 fn create(&self, checkout: &RecordCheckout) -> Result<(), String> {
591 let initialize = |doc: &mut ui_core::automerge::AutoCommit| {
592 P::initialize(doc).map_err(|error| error.to_string())
593 };
594 match self.workspace.store.reserve_record(checkout) {
595 Ok(mut reservation) => reservation
596 .initialize(initialize)
597 .map_err(|error| error.to_string()),
598 Err(RecordOpenError::IdentityMismatch) => {
601 self.workspace
602 .store
603 .create_record(checkout)
604 .map_err(|error| error.to_string())?;
605 self.workspace
606 .store
607 .with(&checkout.id, |doc| {
608 initialize(doc)?;
609 doc.commit();
610 Ok::<(), String>(())
611 })
612 .ok_or("the new part is not open")?
613 }
614 Err(error) => Err(error.to_string()),
615 }
616 }
617
618 fn follow(&self, index: u64) -> Result<(), String> {
619 let part = self.chain.part(index);
620 let older = self
624 .workspace
625 .store
626 .read(&part, tilo_resource::evolution::needs_migrate::<P>)
627 .ok_or("the part is not open here")?;
628 if older {
629 if !self.workspace.store.takes_writes() {
632 return Err("the part is migrated once the store takes writes".into());
633 }
634 self.workspace.store.migrate(&part, |doc| {
635 tilo_resource::evolution::migrate::<P>(doc).map_err(|error| error.to_string())
636 })?;
637 }
638 let reader = self
639 .workspace
640 .store
641 .read(&part, Reader::<P>::read)
642 .ok_or("the part is not open here")?
643 .map_err(|error| error.to_string())?;
644 self.parts.borrow_mut().insert(index, Live::new(reader));
645 self.moved();
646 Ok(())
647 }
648
649 pub(crate) fn apply_as(
654 &self,
655 action: &P::Action,
656 invoker: &ActionInvoker,
657 ) -> Result<u64, ActionError> {
658 self.apply_answering(action, invoker)
659 .map(|(index, _)| index)
660 }
661
662 fn apply_answering(
665 &self,
666 action: &P::Action,
667 invoker: &ActionInvoker,
668 ) -> Result<(u64, Vec<String>), ActionError> {
669 let mut action = action.clone();
670 let base = (self.base)();
671 P::prepare(&mut action, &base).map_err(ActionError::Refused)?;
672 let place = P::place(&action, &base);
673 drop(base);
674 let index = P::part_for(&action, &self.parts()).map_err(ActionError::Refused)?;
675 if let Some(permissions) = &self.workspace.permissions {
676 let need = tilo_resource::need_of::<P>(&action);
680 let node = if let Some(verb) = verb_of::<P>(&action) {
681 tilo_actions::action_node(
682 &TreePath::root(),
683 self.workspace.app_id,
684 P::KIND,
685 &self.id,
686 need.field.as_deref(),
687 &verb,
688 )
689 } else {
690 TreePath::root()
693 .app(self.workspace.app_id)
694 .kind(P::KIND)
695 .resource(self.id.clone())
696 .field(need.field.as_deref().unwrap_or_default())
697 .action(tilo_resource::FIELD_EDIT)
698 };
699 if !permissions.check(&PermissionScope::Action {
700 node,
701 kind: need.kind,
702 }) {
703 return Err(ActionError::Refused(format!(
704 "you may not do this to these {}",
705 P::LABEL.to_lowercase()
706 )));
707 }
708 }
709 let head = self.parts.borrow().keys().next_back().copied();
710 match self
711 .open_or_start(index)
712 .map_err(|error| self.refused(error))?
713 {
714 Opened::Held => {}
715 Opened::Started => {
716 if let Some(previous) = head.filter(|head| head.checked_add(1) == Some(index)) {
717 self.carry(previous, index, invoker)?;
718 }
719 }
720 Opened::Asked => {
721 return Err(ActionError::Refused(
722 "the part is on its way from the workspace".into(),
723 ));
724 }
725 }
726 let encoded = tilo_resource::encode::<P>(&action)?;
727 let created = self.write(index, &action, encoded, invoker, place)?;
728 Ok((index, created))
729 }
730
731 fn carry(&self, previous: u64, index: u64, invoker: &ActionInvoker) -> Result<(), ActionError> {
734 let Some(carried) = self
735 .parts
736 .borrow()
737 .get(&previous)
738 .and_then(|part| P::carry(previous, &part.get()))
739 else {
740 return Ok(());
741 };
742 let action = carried
743 .build(tilo_resource::origin_now_ms())
744 .map_err(ActionError::Refused)?;
745 let encoded = tilo_resource::encode::<P>(&action)?;
746 self.write(index, &action, encoded, invoker, None).map(drop)
747 }
748
749 fn apply_history(
754 &self,
755 edit: &PartEdit,
756 invoker: &ActionInvoker,
757 ) -> Result<serde_json::Value, ActionError> {
758 if let Some(permissions) = &self.workspace.permissions
759 && !permissions.check(&PermissionScope::Action {
760 node: tilo_actions::action_node(
761 &TreePath::root(),
762 self.workspace.app_id,
763 P::KIND,
764 &self.id,
765 None,
766 edit.verb(),
767 ),
768 kind: tilo_actions::ActionKind::Undoable,
769 })
770 {
771 return Err(ActionError::Refused(format!(
772 "you may not change the history of these {}",
773 P::LABEL.to_lowercase()
774 )));
775 }
776 self.hold_kept(edit.part)
777 .map_err(|error| self.refused(error))?;
778 let encoded = edit.encoded().map_err(ActionError::Refused)?;
779 self.write(
780 edit.part,
781 &super::history::part_action::<P>(edit),
782 encoded,
783 invoker,
784 None,
785 )?;
786 let now = self
787 .workspace
788 .store
789 .with(&self.chain.part(edit.part), |doc| {
790 super::history::version_name(&doc.get_heads())
791 })
792 .unwrap_or_default();
793 Ok(serde_json::json!({ "part": edit.part, "now": now }))
794 }
795
796 fn write(
801 &self,
802 index: u64,
803 action: &dyn tilo_actions::Action,
804 encoded: tilo_actions::EncodedAction,
805 invoker: &ActionInvoker,
806 place: Option<String>,
807 ) -> Result<Vec<String>, ActionError> {
808 let mut context = ActionContext::for_resource(&self.id, P::KIND, P::LABEL)
809 .with_app(self.workspace.app_id, self.workspace.app_title)
810 .with_invoker(invoker.clone())
811 .with_change(action.change_kind());
812 if let Some(place) = place.filter(|place| !place.trim().is_empty()) {
813 context = context.with_facet(tilo_actions::ActionFacet::new(
814 tilo_actions::PLACE_FACET,
815 place,
816 ));
817 }
818 let id = self.chain.part(index);
819 let created = self
820 .workspace
821 .store
822 .with_history(&id, |doc, history| {
823 let before = doc.get_heads();
824 let writer = ui_core::bound_dispatch::document_writer(&invoker.actor, doc);
825 let applied = tilo_actions::apply_local_with_context(
826 doc,
827 action,
828 encoded,
829 Some(&writer),
830 context,
831 )
832 .map(|envelope| {
833 ui_core::bound_dispatch::minted_items(doc, P::KIND, &envelope, &before)
834 })
835 .map_err(|error| ActionError::Refused(error.to_string()));
836 history.rebase(doc);
839 applied
840 })
841 .ok_or_else(|| ActionError::Refused("the part is not open here".into()))??;
842 if let Err(error) = self.workspace.store.schedule_committed_history(&id) {
843 tracing::error!(%error, "an edited part could not be saved");
844 }
845 self.refresh();
846 self.moved();
847 Ok(created)
848 }
849
850 pub(crate) fn publish(&self) -> tilo_actions::ResourceSpec {
855 let revision = (self.revision(), ui_core::i18n::I18nManager::revision());
856 if let Some((at, published)) = self.published.borrow().as_ref()
857 && *at == revision
858 {
859 return published.clone();
860 }
861 let parts = self.parts();
862 let head = parts
863 .last()
864 .map_or_else(|| Rc::clone(&self.empty), |(_, state)| Rc::clone(state));
865 let mut resource = tilo_resource::publish::<P>(&self.id, P::LABEL, &head);
866 let mut seen: std::collections::HashSet<tilo_actions::ResourceId> = resource
867 .children
868 .iter()
869 .map(|child| child.id.clone())
870 .collect();
871 for (_, state) in parts.iter().rev().skip(1) {
872 let earlier = tilo_resource::publish::<P>(&self.id, P::LABEL, state);
873 resource.children.extend(
874 earlier
875 .children
876 .into_iter()
877 .filter(|child| seen.insert(child.id.clone())),
878 );
879 }
880 let mut published = route(resource, &self.id);
881 let own: std::collections::HashSet<String> = published
882 .actions
883 .iter()
884 .map(|spec| spec.id.clone())
885 .collect();
886 published.actions.extend(
887 super::history::part_edit_specs()
888 .into_iter()
889 .filter(|spec| !own.contains(&spec.id))
890 .map(|spec| super::record::route_operation(spec, CHAIN_ACTION_TYPE, &self.id)),
891 );
892 *self.published.borrow_mut() = Some((revision, published.clone()));
893 published
894 }
895}
896
897fn verb_of<P: Chained>(action: &P::Action) -> Option<String> {
900 use tilo_actions::Action as _;
901 let mut doc = ui_core::automerge::AutoCommit::new();
902 P::initialize(&mut doc).ok()?;
903 match action.describe(&doc)?.what {
904 tilo_actions::Did::Verb(call) => Some(call.action.action),
905 _ => None,
906 }
907}
908
909fn route(resource: tilo_actions::ResourceSpec, chain: &str) -> tilo_actions::ResourceSpec {
916 route_at(resource, chain, false)
917}
918
919fn route_at(
920 mut resource: tilo_actions::ResourceSpec,
921 chain: &str,
922 item: bool,
923) -> tilo_actions::ResourceSpec {
924 resource.actions = std::mem::take(&mut resource.actions)
925 .into_iter()
926 .filter_map(|spec| {
927 if spec.kind == OperationKind::Document {
928 Some(super::record::route_operation(
929 spec,
930 CHAIN_ACTION_TYPE,
931 chain,
932 ))
933 } else {
934 item.then_some(spec)
935 }
936 })
937 .collect();
938 resource.children = std::mem::take(&mut resource.children)
939 .into_iter()
940 .map(|child| route_at(child, chain, true))
941 .collect();
942 resource
943}
944
945impl<P: Chained> RecordDoor for ChainState<P> {
946 fn id(&self) -> &str {
947 &self.id
948 }
949
950 fn editable(&self) -> bool {
951 true
952 }
953
954 fn revision(&self) -> u64 {
955 ChainState::revision(self)
956 }
957
958 fn publish(&self) -> tilo_actions::ResourceSpec {
959 ChainState::publish(self)
960 }
961
962 fn apply_encoded(
963 &self,
964 action: &tilo_actions::EncodedAction,
965 invoker: &ActionInvoker,
966 ) -> Result<(), String> {
967 if action.type_id == PART_EDIT_TYPE {
968 let edit: PartEdit =
969 tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
970 return self
971 .apply_history(&edit, invoker)
972 .map(drop)
973 .map_err(|error| error.to_string());
974 }
975 if action.type_id != P::ACTION_TYPE {
976 return Err(format!("{} take no `{}` action", P::LABEL, action.type_id));
977 }
978 let action: P::Action =
979 tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
980 self.apply_as(&action, invoker)
981 .map(drop)
982 .map_err(|error| error.to_string())
983 }
984
985 fn apply_answering(
986 &self,
987 action: &tilo_actions::EncodedAction,
988 invoker: &ActionInvoker,
989 ) -> Result<tilo_actions::ActionOutput, String> {
990 if action.type_id == PART_EDIT_TYPE {
991 let edit: PartEdit =
992 tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
993 let value = self
994 .apply_history(&edit, invoker)
995 .map_err(|error| error.to_string())?;
996 return Ok(tilo_actions::ActionOutput {
997 value: Some(value),
998 resources: Vec::new(),
999 });
1000 }
1001 if action.type_id != P::ACTION_TYPE {
1002 return Err(format!("{} take no `{}` action", P::LABEL, action.type_id));
1003 }
1004 let action: P::Action =
1005 tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
1006 let (_, resources) = ChainState::apply_answering(self, &action, invoker)
1007 .map_err(|error| error.to_string())?;
1008 Ok(tilo_actions::ActionOutput {
1009 value: None,
1010 resources,
1011 })
1012 }
1013}
1014
1015pub struct Chain<P: Chained> {
1018 state: Rc<ChainState<P>>,
1019 dispatch: Dispatcher<P>,
1020}
1021
1022impl<P: Chained> Clone for Chain<P> {
1023 fn clone(&self) -> Self {
1024 Self {
1025 state: Rc::clone(&self.state),
1026 dispatch: self.dispatch.clone(),
1027 }
1028 }
1029}
1030
1031impl<P: Chained> Chain<P> {
1032 pub(crate) fn new(state: Rc<ChainState<P>>, dispatch: Dispatcher<P>) -> Self {
1033 Self { state, dispatch }
1034 }
1035
1036 pub(crate) fn applying(state: &Rc<ChainState<P>>, app_id: &'static str, me: &str) -> Self {
1041 Self::new(Rc::clone(state), detached_dispatch(app_id, state, me))
1042 }
1043
1044 #[must_use]
1047 pub fn id(&self) -> &str {
1048 self.state.id()
1049 }
1050
1051 #[must_use]
1053 pub fn parts(&self) -> Vec<(u64, Rc<P::State>)> {
1054 self.state.parts()
1055 }
1056
1057 #[must_use]
1060 pub fn head(&self) -> Option<u64> {
1061 self.state.head()
1062 }
1063
1064 #[must_use]
1066 pub fn revision(&self) -> u64 {
1067 self.state.revision()
1068 }
1069
1070 pub fn hold(&self, index: u64) -> Result<(), String> {
1075 self.state.hold(index)
1076 }
1077
1078 #[must_use]
1082 pub fn waiting(&self, index: u64) -> Option<PartArrival> {
1083 self.state.refresh();
1084 self.state.waiting(index)
1085 }
1086
1087 #[must_use]
1089 pub fn waited(&self) -> Vec<(u64, PartArrival)> {
1090 self.state.refresh();
1091 self.state.waited()
1092 }
1093
1094 pub fn apply(&self, action: &P::Action) -> Result<u64, ActionError> {
1101 let me = self.dispatch.me().unwrap_or(&self.state.workspace.me);
1102 self.state.apply_as(action, &ActionInvoker::actor(me))
1103 }
1104
1105 pub fn invoke(&self, invoke: &Invoke<P>) -> Result<u64, ActionError> {
1110 let action = invoke
1111 .build(tilo_resource::origin_now_ms())
1112 .map_err(ActionError::Refused)?;
1113 self.apply(&action)
1114 }
1115
1116 pub fn undo_in(&self, index: u64) -> Result<(), ActionError> {
1128 let me = self.dispatch.me().unwrap_or(&self.state.workspace.me);
1129 let edit = PartEdit {
1130 part: index,
1131 edit: super::history::HistoryEdit::Undo {
1132 steps: None,
1133 run: None,
1134 by: None,
1135 to: None,
1136 step: None,
1137 },
1138 };
1139 let undone = self
1140 .state
1141 .apply_history(&edit, &ActionInvoker::actor(me))
1142 .map(drop);
1143 if let Err(error) = &undone {
1144 self.dispatch.refused().set(Some(super::dispatch::Refused {
1145 action: edit.verb().to_owned(),
1146 item: None,
1147 reason: error.to_string(),
1148 by_workspace: false,
1149 }));
1150 }
1151 undone
1152 }
1153
1154 pub fn stage_as(&self, invoke: &Invoke<P>, author: &str) -> Result<u64, ActionError> {
1162 self.stage_at(invoke, author, tilo_resource::origin_now_ms())
1163 }
1164
1165 pub fn stage_at(
1171 &self,
1172 invoke: &Invoke<P>,
1173 author: &str,
1174 now_ms: i64,
1175 ) -> Result<u64, ActionError> {
1176 let action = invoke.build(now_ms).map_err(ActionError::Refused)?;
1177 self.state.apply_as(&action, &ActionInvoker::actor(author))
1178 }
1179
1180 pub fn write_start(&self, index: u64, state: &P::State) -> Result<(), String>
1187 where
1188 P::State: tilo_resource::Fields,
1189 {
1190 use tilo_resource::Fields as _;
1191 self.state.start(index)?;
1192 let id = self.state.chain.part(index);
1193 self.state
1194 .workspace
1195 .store
1196 .with_history(&id, |doc, history| {
1197 let root = ui_core::automerge::ROOT;
1198 let written = <P::State as tilo_resource::Fields>::read_fields(doc, &root)
1199 .and_then(|before| {
1200 state.write_changed_fields(
1201 &before,
1202 doc,
1203 &root,
1204 &tilo_resource::Delta::default(),
1205 )
1206 })
1207 .map_err(|error| error.to_string());
1208 doc.commit_with(
1209 ui_core::automerge::transaction::CommitOptions::default()
1210 .with_message(tilo_actions::FrameworkChange::Genesis.change_message()),
1211 );
1212 history.rebase(doc);
1213 written
1214 })
1215 .ok_or("the part is not open here")??;
1216 if let Err(error) = self.state.workspace.store.schedule_committed_history(&id) {
1217 tracing::error!(%error, "a created part could not be saved");
1218 }
1219 self.state.refresh();
1220 self.state.moved();
1221 Ok(())
1222 }
1223
1224 #[must_use]
1228 pub fn takes_changes(&self) -> bool {
1229 self.state.workspace.store.takes_writes()
1230 }
1231
1232 #[must_use]
1236 pub fn dispatcher(&self) -> Dispatcher<P> {
1237 self.dispatch.clone()
1238 }
1239
1240 #[must_use]
1246 pub fn detached(
1247 app_id: &'static str,
1248 me: &str,
1249 base: impl Fn() -> Rc<<P::Base as Resource>::State> + 'static,
1250 ) -> Option<Self> {
1251 Self::detached_in(RecordStore::in_memory(), app_id, me, base)
1252 }
1253
1254 #[must_use]
1258 pub fn detached_in(
1259 store: RecordStore,
1260 app_id: &'static str,
1261 me: &str,
1262 base: impl Fn() -> Rc<<P::Base as Resource>::State> + 'static,
1263 ) -> Option<Self> {
1264 let workspace = WorkspaceRecords {
1265 store,
1266 me: me.to_owned(),
1267 permissions: None,
1268 app_id,
1269 app_title: app_id,
1270 };
1271 let state = Rc::new(ChainState::<P>::open(workspace, Rc::new(base))?);
1272 let dispatch = detached_dispatch(app_id, &state, me);
1273 Some(Self { state, dispatch })
1274 }
1275
1276 #[must_use]
1279 pub fn as_reader(&self, me: &str) -> Self {
1280 Self {
1281 state: Rc::clone(&self.state),
1282 dispatch: detached_dispatch(self.state.workspace.app_id, &self.state, me),
1283 }
1284 }
1285
1286 #[must_use]
1289 pub fn part_bytes(&self, index: u64) -> Option<Vec<u8>> {
1290 self.state
1291 .workspace
1292 .store
1293 .read(&self.state.chain.part(index), |doc| doc.clone().save())
1294 }
1295
1296 #[cfg(any(test, feature = "test-support"))]
1303 pub fn confirm_asked(&self) -> Result<(), ActionError> {
1304 for action in self.dispatch.take_asked().map_err(ActionError::Refused)? {
1305 self.apply(&action)?;
1306 }
1307 Ok(())
1308 }
1309}
1310
1311fn detached_dispatch<P: Chained>(
1313 app_id: &'static str,
1314 state: &Rc<ChainState<P>>,
1315 me: &str,
1316) -> Dispatcher<P> {
1317 let applying = Rc::downgrade(state);
1318 let invoker = ActionInvoker::actor(me);
1319 Dispatcher::<P>::new(app_id, state.id(), false, Some(me)).for_workspace_record::<P>(
1320 state.id(),
1321 false,
1322 move |action| {
1323 let state = applying
1324 .upgrade()
1325 .ok_or_else(|| ActionError::Refused("the chain is closed".into()))?;
1326 state.apply_as(action, &invoker).map(drop)
1327 },
1328 )
1329}
1330
1331type HeldChains = BTreeMap<(&'static str, String), Rc<dyn Any>>;
1335
1336#[derive(Default)]
1337pub(crate) struct Chains {
1338 held: RefCell<HeldChains>,
1339 children: RefCell<BTreeMap<String, Rc<dyn Any>>>,
1341 doors: RefCell<Vec<Rc<dyn RecordDoor>>>,
1342}
1343
1344impl Chains {
1345 pub(crate) fn open<P: Chained>(
1347 &self,
1348 workspace: Option<&WorkspaceRecords>,
1349 base: impl Fn() -> Rc<<P::Base as Resource>::State> + 'static,
1350 ) -> Option<Rc<ChainState<P>>> {
1351 let chain = P::chain();
1352 if let Some(held) = self.opened::<P>(&chain) {
1353 return Some(held);
1354 }
1355 let workspace = workspace?.clone();
1356 let state = Rc::new(ChainState::<P>::open(workspace, Rc::new(base))?);
1357 self.hold(&chain, &state);
1358 Some(state)
1359 }
1360
1361 pub(crate) fn open_beside<P: Chained>(
1364 &self,
1365 workspace: Option<&WorkspaceRecords>,
1366 base: &RecordId,
1367 base_state: impl Fn() -> Rc<<P::Base as Resource>::State> + 'static,
1368 ) -> Option<Rc<ChainState<P>>> {
1369 self.open_at::<P>(workspace?, &P::chain_beside(base), base_state)
1370 }
1371
1372 pub(crate) fn open_at<P: Chained>(
1376 &self,
1377 workspace: &WorkspaceRecords,
1378 chain: &RecordId,
1379 base_state: impl Fn() -> Rc<<P::Base as Resource>::State> + 'static,
1380 ) -> Option<Rc<ChainState<P>>> {
1381 if let Some(held) = self.opened::<P>(chain) {
1382 return Some(held);
1383 }
1384 let state = Rc::new(ChainState::<P>::open_at(
1385 workspace.clone(),
1386 Rc::new(base_state),
1387 chain.clone(),
1388 )?);
1389 self.hold(chain, &state);
1390 Some(state)
1391 }
1392
1393 fn opened<P: Chained>(&self, chain: &RecordId) -> Option<Rc<ChainState<P>>> {
1395 let held = self.held.borrow();
1396 let held = held.get(&(P::KIND, chain.as_str().to_owned()))?;
1397 Rc::clone(held).downcast::<ChainState<P>>().ok()
1398 }
1399
1400 fn hold<P: Chained>(&self, chain: &RecordId, state: &Rc<ChainState<P>>) {
1403 self.held.borrow_mut().insert(
1404 (P::KIND, chain.as_str().to_owned()),
1405 Rc::clone(state) as Rc<dyn Any>,
1406 );
1407 self.doors
1408 .borrow_mut()
1409 .push(Rc::clone(state) as Rc<dyn RecordDoor>);
1410 }
1411
1412 pub(crate) fn open_children<C: super::children::ChildRecord>(
1415 &self,
1416 workspace: Option<&WorkspaceRecords>,
1417 parent: impl Fn() -> Rc<<C::Parent as Resource>::State> + 'static,
1418 ) -> Option<Rc<super::children::ChildrenState<C>>> {
1419 let key = ui_core::record::children_resource_id(&C::parent_record(), C::KIND);
1420 if let Some(held) = self.children.borrow().get(&key) {
1421 return Rc::clone(held)
1422 .downcast::<super::children::ChildrenState<C>>()
1423 .ok();
1424 }
1425 let state = Rc::new(super::children::ChildrenState::<C>::open(
1426 workspace?.clone(),
1427 Rc::new(parent),
1428 )?);
1429 self.children
1430 .borrow_mut()
1431 .insert(key, Rc::clone(&state) as Rc<dyn Any>);
1432 self.doors
1433 .borrow_mut()
1434 .push(Rc::clone(&state) as Rc<dyn RecordDoor>);
1435 Some(state)
1436 }
1437
1438 pub(crate) fn doors(&self) -> Vec<Rc<dyn RecordDoor>> {
1439 self.doors.borrow().clone()
1440 }
1441
1442 pub(crate) fn answer(
1445 &self,
1446 me: Option<&str>,
1447 payload: &[u8],
1448 live: &mut ui_core::widget::Edits<'_>,
1449 ) -> Result<tilo_actions::ActionOutput, String> {
1450 let request: super::record::RecordAction =
1451 tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
1452 let door = self
1453 .doors
1454 .borrow()
1455 .iter()
1456 .find(|door| door.id() == request.record)
1457 .cloned()
1458 .ok_or_else(|| format!("no chain `{}` is open here", request.record))?;
1459 let invoker = ui_core::invocation_scope::current_invoker()
1460 .or_else(|| me.map(ActionInvoker::actor))
1461 .ok_or("nobody changes a past version")?;
1462 door.apply_in(&request.action, &invoker, live)
1463 }
1464}