Skip to main content

tilo_app/framework/
chain.rs

1//! Records an app keeps beside its own as a chain of parts: what grows
2//! without end — a chat's messages, a log — kept as numbered records of
3//! another resource type ([`RecordId::part`]) in the workspace the app's record
4//! lives in.
5//!
6//! One record cannot hold everything such an app ever wrote: a record's
7//! snapshot has a size limit, and a newcomer would be sent all of it. A
8//! chain keeps it in parts. The part with the highest number a device holds
9//! is the chain's head; a workspace's host replays a joining device the head
10//! only, and an earlier part reaches a device when it opens it
11//! ([`Chain::hold`]). Two writers that start the same new part start the
12//! same record (each part's genesis is the same everywhere), so their
13//! changes meet there.
14//!
15//! The resource of the parts says how they are used ([`Chained`]): which
16//! part an action changes (the one holding the item it names, or for a new
17//! item the head or the part after it), and what an action reads of the
18//! app's own record where it is invoked. Everything else is the framework's:
19//!
20//! - **One door.** The chain is published beside the app's resource as one
21//!   resource ([`Chain::id`]): the head's actions, and every held part's
22//!   items with theirs, each carried out on the part [`Chained::part_for`]
23//!   names when it is invoked. The palette, agents, scripts and the app's
24//!   own surface reach the parts through it, checked against the
25//!   participant's roles in the workspace like the app's own actions (the parts
26//!   carry the app record's grants), attributed to whoever invokes them and
27//!   written into the part's history.
28//! - **Held where the app is.** The parts are records of the app's own
29//!   store, so whatever syncs the app's records — the browser's workspace, an
30//!   attached view — syncs them on their own routes.
31//! - **Read as they change.** [`Chain::parts`] is every part the window
32//!   holds, oldest first, followed change by change; [`Chain::revision`]
33//!   moves whenever any of them changed, or a new head arrived.
34//!
35//! An app declares its chain in [`ResourceApp::records`](super::ResourceApp::records)
36//! (`records.chain::<Part>()`), so it is opened and published with the
37//! window, and reaches it from a view or surface with
38//! [`RecordEnv::chain`](super::RecordEnv::chain). Edits of a part are not
39//! undone with the app's own: they are recorded in the part's history, and
40//! the chain publishes the history verbs (`undo`, `redo`, `restore`,
41//! `checkpoint`) naming the part they change, so a person, an agent or the
42//! workspace's moderation takes back steps in a part through the same door.
43//!
44//! A part opened in the app's store by anyone — a peer's merged into an
45//! attached view, one this device asked the workspace for — is held from the
46//! next read on.
47
48use 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
63/// The operation type a chain's published actions are carried out under:
64/// the host applies the action to the part of the chain it names.
65pub(crate) const CHAIN_ACTION_TYPE: &str = "tilo.app.chain-action";
66
67/// Where an earlier part a window asked for ([`Chain::hold`]) stands until
68/// what the workspace has of it is read here.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum PartArrival {
71    /// Asked for; what the workspace has of it is on its way.
72    Arriving,
73    /// The workspace's hosts sent everything they hold, and none of it was this
74    /// part, or no host is connected to ask: nothing more comes until that
75    /// changes (a host connects, someone who has it shares it).
76    Unavailable,
77}
78
79/// A resource whose records are the parts of a chain kept beside the app's
80/// own record ([`RecordId::part`]). See the module docs.
81pub trait Chained: Resource {
82    /// The app's own resource: what [`Self::prepare`] reads where an action
83    /// is invoked.
84    type Base: Resource;
85
86    /// The chain the parts belong to: part `n` is `chain().part(n)`.
87    fn chain() -> RecordId;
88
89    /// The chain that belongs beside `base` rather than beside the app's own
90    /// record: the messages of one conversation, kept beside its record
91    /// ([`RecordId::of_direct`]). [`Self::chain`] for every chain there is
92    /// only one of.
93    fn chain_beside(base: &RecordId) -> RecordId {
94        let _ = base;
95        Self::chain()
96    }
97
98    /// The part `action` changes, given the parts this device holds, oldest
99    /// first: the one holding the item it names, or for a new item the head
100    /// (or the next part, once the head is full).
101    fn part_for(action: &Self::Action, parts: &[(u64, Rc<Self::State>)]) -> Result<u64, String>;
102
103    /// Fill in, where the action is invoked, what it records from the app's
104    /// own record (who a mention names), and refuse what that record does
105    /// not allow (a post to a channel it has not got). A part's action
106    /// itself reads only its part, so it replays the same on every replica.
107    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    /// What a part started after part `previous` (whose state is `part`)
116    /// carries of the parts before it, written into it as it starts, before
117    /// the action that started it: a summary a reader needs of parts it
118    /// does not hold (a chat's message counts for unread badges). Whoever
119    /// starts a part holds the one before. `None` carries nothing.
120    fn carry(previous: u64, part: &Self::State) -> Option<Invoke<Self>> {
121        let _ = (previous, part);
122        None
123    }
124
125    /// Where in the app's own record `action` happens, named as everyone
126    /// the record is shared with sees that place (a post's channel,
127    /// `#general`): history keeps it on the step
128    /// ([`tilo_actions::PLACE_FACET`]), so what someone is doing can say
129    /// where. `None` for an action of no such place, and for a place not
130    /// everyone sees (a direct conversation).
131    fn place(action: &Self::Action, base: &<Self::Base as Resource>::State) -> Option<String> {
132        let _ = (action, base);
133        None
134    }
135}
136
137/// How a part was opened ([`ChainState::open_or_start`]).
138enum Opened {
139    /// Held already, or read from this device.
140    Held,
141    /// Started from its genesis here: nothing of it was on this device.
142    Started,
143    /// Asked of the workspace, through which the store reads records; not open
144    /// until it arrives.
145    Asked,
146}
147
148/// Why a part is not held here, and whether that is only for this moment.
149struct Unopened {
150    reason: String,
151    /// Another process held the part's store for a write while it was looked
152    /// for (a save, the workspace adopting the part): the part opens at the
153    /// next refresh, so a view keeps waiting for it rather than being told
154    /// it is unavailable.
155    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/// What a window holds a chain with: the store of the app's records, who
177/// edits, and what they may do in the workspace.
178#[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
187/// The app's record as a part's [`Chained::prepare`] reads it.
188type Base<P> = Rc<dyn Fn() -> Rc<<<P as Chained>::Base as Resource>::State>>;
189
190/// A chain held for a window: its parts, read as they change. See the
191/// [module docs](self).
192pub(crate) struct ChainState<P: Chained> {
193    workspace: WorkspaceRecords,
194    base: Base<P>,
195    /// The chain the parts belong to.
196    chain: RecordId,
197    id: String,
198    kind: RecordKind,
199    parts: RefCell<BTreeMap<u64, Live<P>>>,
200    /// Earlier parts asked for ([`Self::hold`]) that are not read here
201    /// yet: held from their genesis, or not open at all while the workspace
202    /// sends them.
203    waiting: RefCell<BTreeMap<u64, PartArrival>>,
204    /// What a part holds before anything was written to it: what the chain
205    /// publishes before it has a head.
206    empty: Rc<P::State>,
207    revision: Cell<u64>,
208    /// The store's [`RecordStore::kept_revision`] when the window last
209    /// looked for a newer head and at the parts it waits for.
210    looked: Cell<Option<u64>>,
211    published: RefCell<Option<((u64, u64), tilo_actions::ResourceSpec)>>,
212}
213
214impl<P: Chained> ChainState<P> {
215    /// Hold the chain in `workspace`, with its head when this device has one.
216    pub(crate) fn open(workspace: WorkspaceRecords, base: Base<P>) -> Option<Self> {
217        Self::open_at(workspace, base, P::chain())
218    }
219
220    /// [`Self::open`] of the chain `chain` rather than the app's own: the
221    /// messages of one direct conversation, which lives beside the record
222    /// the two people share ([`Chained::chain_beside`]).
223    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    /// The parts held, oldest first.
264    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    /// Follow every held part that changed, and take up a newer head that
288    /// arrived on this device. Whether anything moved.
289    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                    // Its history arrived, or a first change was made here.
299                    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        // A part opened in the store since (a peer's merged in, one this
310        // device asked for and received) is held at once.
311        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    /// Whether to look for a newer head and at the parts waited for now:
334    /// the store was told a record arrived or what the workspace said of one
335    /// ([`RecordStore::note_kept`], [`RecordStore::announce`],
336    /// [`RecordStore::note_workspace_answered`]), each of which wakes the window.
337    /// Not while a frame reads the store (a view's sync): opening a part
338    /// takes the store's writer, so the look waits for the frame's settle,
339    /// where the window takes up what it was sent, and the view's sync reads
340    /// the part it opened.
341    fn due(&self) -> bool {
342        self.workspace.store.takes_writes()
343            && self.looked.get() != Some(self.workspace.store.kept_revision())
344    }
345
346    /// Open every part waited for that arrived since, and mark unavailable
347    /// those the workspace said it has no more of. Whether any changed.
348    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                    // Here now, with what the workspace sent of it.
361                    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                        // Another writer held the part's store just now: it is
372                        // waited for again at the next refresh.
373                        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    /// Open every part newer than the head that this device holds. Whether
399    /// one was.
400    fn look(&self) -> bool {
401        let looked = self.workspace.store.kept_revision();
402        let head = self.parts.borrow().keys().next_back().copied();
403        // Only the newest is the head; the parts between are opened when
404        // someone reads back to them.
405        let kept = self.workspace.store.held_parts(&self.chain).last().copied();
406        // A part a peer started is the same record here (each part's genesis
407        // is the same everywhere): started here, it syncs on its route.
408        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            // A part a peer started, started here from its genesis: what
426            // they wrote in it is on its way, and a view of it waits for
427            // that rather than reading back from an empty head.
428            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                // A part another writer held for this moment is not gone: it
449                // is looked for again at the next refresh, rather than only
450                // once this device is told of something new. Nothing is
451                // marked moved, so a failed look wakes nobody and cannot
452                // make a pass of its own.
453                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    /// Refuse an action on a part that is not held here, arming the next
461    /// refresh to take it up when another writer held its store for this
462    /// moment ([`Unopened::retry`]).
463    fn refused(&self, error: Unopened) -> ActionError {
464        if error.retry {
465            self.looked.set(None);
466        }
467        ActionError::Refused(error.reason)
468    }
469
470    /// Hold part `index`, which this device keeps: a part a history verb
471    /// names is one someone here wrote to.
472    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    /// Hold part `index`: read it from this device, or, when this device
490    /// has none of it, ask the workspace for it. A store that reads records
491    /// through the workspace (a native app's) asks for it as it opens it; any
492    /// other starts it from its genesis (the same change on every device),
493    /// so it is open here and the workspace's record sync asks the workspace for its
494    /// history, which merges in as it arrives. It is waited for until then
495    /// ([`Self::waiting`]); its arrival, or the workspace saying it has none of
496    /// it, wakes the window ([`RecordStore::wake_on_arrival`]).
497    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                // Looked at again at the next refresh, with what the store
505                // already knows of it.
506                self.looked.set(None);
507                self.moved();
508            }
509            Err(Unopened {
510                retry: true,
511                reason,
512            }) => {
513                // Another writer holds the part's store for this moment: the
514                // part is on its way here, and reading it at the next
515                // refresh succeeds.
516                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    /// Where part `index`, asked for and not read here yet, stands; `None`
529    /// once it is read (or was never asked for).
530    pub(crate) fn waiting(&self, index: u64) -> Option<PartArrival> {
531        self.waiting.borrow().get(&index).copied()
532    }
533
534    /// Every part asked for and not read here yet, oldest first.
535    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    /// Hold part `index`, starting it when nobody has.
544    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                    // Another writer's write is done by the next refresh,
551                    // which holds the part then.
552                    self.looked.set(None);
553                }
554                Err(error.reason)
555            }
556        }
557    }
558
559    /// Hold part `index`, kept on this device or started from its genesis,
560    /// or ask the workspace for it where the store reads records through it.
561    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                // Nothing of it on this device, or a store that keeps no
572                // canonical records (a test's) and has no record it could
573                // open: it is started as every record is.
574                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    /// Create the part `checkout` names, with the part resource's genesis:
589    /// the same on every device, so two that start it converge.
590    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            // A store that keeps no canonical records (a test's) creates it
599            // the way it creates every record.
600            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        // An older part is brought up to the part's storage version before
621        // it is read, as the app's record is when it opens: once, as the
622        // same housekeeping change on every device.
623        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            // Not while a frame reads the store: the part is held at the
630            // next refresh, as a part this device asked for is.
631            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    /// Apply `action`, invoked by `invoker`, to the part it changes: read
650    /// against the app's record ([`Chained::prepare`]), checked against
651    /// what they may do in the workspace, attributed to them and written into
652    /// the part's history. The part it was applied to.
653    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    /// [`Self::apply_as`], with the item resources the action created: what
663    /// its invoker is answered, as for the app's own actions.
664    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            // Decided where the chain publishes the action, as the door
677            // decides it: a grant on the app, the parts' kind or the verb
678            // covers it.
679            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                // A field edit, at the field it changes, as the published
691                // `field-edit`: a grant on that action covers it too.
692                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    /// Write into part `index`, just started after part `previous`, what it
732    /// carries of the parts before it ([`Chained::carry`]), as `invoker`'s.
733    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    /// Apply a history verb to the part it names ([`PartEdit`]), invoked by
750    /// `invoker`: checked as an undoable change where the chain publishes
751    /// the verb, and written into the part's history as any of its steps.
752    /// Answers the part and its version after.
753    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    /// Write `action` into part `index`, which is open here, attributed to
797    /// `invoker` and recorded in the part's history, with where in the
798    /// app's record it happened (`place`, [`Chained::place`]). The item
799    /// resources it created.
800    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                // Nobody's undo step: a part's edits are kept in its
837                // history, not the app's undo.
838                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    /// The chain as it is published: the head's actions and every held
851    /// part's items, each carried out through [`CHAIN_ACTION_TYPE`] on the
852    /// part it changes; checked against the participant's roles in the workspace
853    /// as the app's own actions are.
854    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
897/// The verb `action` runs, as history names it: what a grant on one verb
898/// covers. `None` for a field edit.
899fn 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
909/// `resource`'s document actions, and those of its children, carried out on
910/// the chain `chain`, each keeping the need it declares. An item's views
911/// and the app's other effects on it stay as the part publishes them,
912/// answered by the app's window: an item of a part opens at the address a
913/// workspace's host knows it by. The chain itself offers only document actions;
914/// the app's own record offers its effects.
915fn 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
1015/// A chain of records the app keeps beside its own, held for a view or
1016/// surface. See the module docs.
1017pub 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    /// The chain an app holds outside a view, beside one of the workspace's
1037    /// records ([`WorkspaceHandle::chain_beside`](super::WorkspaceHandle::chain_beside)):
1038    /// its actions are applied as `me` when the app asks for them
1039    /// ([`Self::invoke`]), not submitted to a host's door.
1040    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    /// The address the chain is published under: what an agent or a
1045    /// script invokes its actions on.
1046    #[must_use]
1047    pub fn id(&self) -> &str {
1048        self.state.id()
1049    }
1050
1051    /// Every part this window holds, oldest first, as it stands now.
1052    #[must_use]
1053    pub fn parts(&self) -> Vec<(u64, Rc<P::State>)> {
1054        self.state.parts()
1055    }
1056
1057    /// The number of the chain's head: the newest part held. `None` while
1058    /// nothing was written to the chain.
1059    #[must_use]
1060    pub fn head(&self) -> Option<u64> {
1061        self.state.head()
1062    }
1063
1064    /// Increases whenever a held part changed or a newer head arrived.
1065    #[must_use]
1066    pub fn revision(&self) -> u64 {
1067        self.state.revision()
1068    }
1069
1070    /// Hold the earlier part `index` too: read it from this device, or ask
1071    /// the workspace for it when this device has none of it. Its history merges
1072    /// in as it arrives ([`Self::revision`] moves, the window is woken);
1073    /// [`Self::waiting`] says where it stands until then.
1074    pub fn hold(&self, index: u64) -> Result<(), String> {
1075        self.state.hold(index)
1076    }
1077
1078    /// Where part `index`, asked for and not read here yet, stands: on its
1079    /// way (a view shows it loading, not empty), or unavailable (a view says
1080    /// so, with nothing spinning). `None` once it is read.
1081    #[must_use]
1082    pub fn waiting(&self, index: u64) -> Option<PartArrival> {
1083        self.state.refresh();
1084        self.state.waiting(index)
1085    }
1086
1087    /// Every part asked for and not read here yet, oldest first.
1088    #[must_use]
1089    pub fn waited(&self) -> Vec<(u64, PartArrival)> {
1090        self.state.refresh();
1091        self.state.waited()
1092    }
1093
1094    /// Apply `action` now, as the window's person, to the part it changes.
1095    /// The part's number.
1096    ///
1097    /// # Errors
1098    /// As the door refuses it: the person may not, the app's record or the
1099    /// part refused it.
1100    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    /// Apply what `invoke` names now, as [`Self::apply`].
1106    ///
1107    /// # Errors
1108    /// As [`Self::apply`], or the arguments do not build the action.
1109    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    /// Take back the window's person's last step in part `index`, through
1117    /// the part's history as the `undo` verb the chain publishes on it does:
1118    /// the way back a surface offers for a step it took at the person's own
1119    /// gesture (Undo beside a message they deleted).
1120    ///
1121    /// A refusal is reported through the dispatcher's
1122    /// [`refused`](Dispatcher::refused), as the door's are.
1123    ///
1124    /// # Errors
1125    /// As the door refuses it: the person may not change the part's history,
1126    /// the part is not here, or they have no step in it to take back.
1127    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    /// Apply what `invoke` names as `author` wrote it: a screenshot
1155    /// scenario staging what other people wrote, or a test acting as
1156    /// someone else. Checked and recorded as [`Self::apply`] is, attributed
1157    /// to `author`.
1158    ///
1159    /// # Errors
1160    /// As [`Self::invoke`].
1161    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    /// [`Self::stage_as`] at `now_ms`, the action's time: what someone
1166    /// wrote at another time.
1167    ///
1168    /// # Errors
1169    /// As [`Self::invoke`].
1170    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    /// Write `state` into part `index` directly, as what the part starts
1181    /// with: a capture's or a test's fixture (items under fixed ids,
1182    /// written by several people) in fields the parts keep to their own
1183    /// actions (`#[model(readonly)]`), which refuse every generic edit.
1184    /// Not an action: nothing checks who may, nobody is its author, and it
1185    /// is committed as the part's genesis, so only a fixture writes it.
1186    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    /// Whether the chain takes a change now: not while a frame reads the
1225    /// app's records (a view's sync), when [`Self::stage_as`] waits for a
1226    /// later pass (an event, a settle pass).
1227    #[must_use]
1228    pub fn takes_changes(&self) -> bool {
1229        self.state.workspace.store.takes_writes()
1230    }
1231
1232    /// The chain's dispatcher: what it submits goes through the host's door
1233    /// as a click on a control does (a destructive verb asks first), and
1234    /// [`apply_now`](Dispatcher::apply_now) applies at once.
1235    #[must_use]
1236    pub fn dispatcher(&self) -> Dispatcher<P> {
1237        self.dispatch.clone()
1238    }
1239
1240    /// A chain outside any host, for a tool or a test that holds the app's
1241    /// records without a window: held in a store of its own, edited as `me`
1242    /// (or whoever [`Self::stage_as`] names), who may do anything, its
1243    /// actions reading the app's record from `base`. `None` when `P` does
1244    /// not read as a part.
1245    #[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    /// [`Self::detached`], held in `store`: one device's records of the
1255    /// app in a workspace, for a test of what reaches that device's store from
1256    /// another one.
1257    #[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    /// The same detached chain edited as `me`: another person's window
1277    /// over it, in a test.
1278    #[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    /// Part `index` as a document's bytes, for a tool that carries it to
1287    /// another device itself; `None` when the chain holds no such part.
1288    #[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    /// What this chain's dispatcher handed to the door and nothing took yet
1297    /// (a destructive verb waiting for confirmation), applied as the door
1298    /// does once the person agreed. Taking them empties the queue.
1299    ///
1300    /// # Errors
1301    /// The first that did not build or apply.
1302    #[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
1311/// The dispatcher of a chain outside any host, applying as `me`.
1312fn 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
1331/// The chains a window's views opened, by part type and chain: one per each;
1332/// and the child records they opened, by container.
1333/// The chains a window holds, by part type and chain.
1334type HeldChains = BTreeMap<(&'static str, String), Rc<dyn Any>>;
1335
1336#[derive(Default)]
1337pub(crate) struct Chains {
1338    held: RefCell<HeldChains>,
1339    /// The child records held, by their container's address.
1340    children: RefCell<BTreeMap<String, Rc<dyn Any>>>,
1341    doors: RefCell<Vec<Rc<dyn RecordDoor>>>,
1342}
1343
1344impl Chains {
1345    /// The chain of `P`, opened in `workspace` on first use.
1346    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    /// The chain of `P` that lives beside the record `base`
1362    /// ([`Chained::chain_beside`]), opened in `workspace` on first use.
1363    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    /// The chain of `P` kept at `chain` in `workspace`, opened on first use.
1373    /// The base is one of the app's own record, or a record of a workspace it
1374    /// keeps beside its own (a chat, whose parts hold its messages).
1375    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    /// The chain of `P` held for `chain`, if the views opened it.
1394    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    /// Keep `state` for the views that ask for its chain again, published
1401    /// and answered beside the others.
1402    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    /// The children of `C` the parent lists, held in `workspace` on first use:
1413    /// published and answered beside the chains, through the same door.
1414    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    /// Answer one published action of a chain: apply it to the part it
1443    /// changes, for whoever the host admitted to invoke it.
1444    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}