Skip to main content

tilo_app/framework/
person.rs

1//! Records an app opens beside the one it shows: its own record of a kind
2//! (what was watched, the words taught to the keyboard, where reading
3//! stopped), kept with its other records in the workspace it is open in,
4//! and the person's shared records another app keeps for all of theirs
5//! (their accounts, their contacts), kept in the person's own scope.
6
7use std::any::Any;
8use std::cell::RefCell;
9use std::collections::HashMap;
10use std::rc::Rc;
11
12use tilo_actions::{Action as _, ActionContext, ActionError, ActionInvoker, ChangeKind};
13
14use super::live::Live;
15use super::record::{Held, RecordDoor};
16use super::section::{SectionDoor, SectionKind};
17use tilo_resource::{Invoke, Resource};
18use ui_core::LocalState;
19use ui_core::history_mode::HistoryOwner;
20use ui_core::record::RecordKind;
21use ui_core::record::StoredRecord;
22use ui_core::record::person::PersonRecords;
23
24/// A record the app keeps beside the one it shows: one per resource kind
25/// and app in the workspace the app is open in, the same on every device,
26/// so it reaches the person's other devices with the workspace. What was
27/// watched, the words taught to the keyboard, where reading stopped. Or
28/// one of the person's shared records another app keeps
29/// ([`tilo_resource::SharedRecord`]). The shared ones belong to the person
30/// rather than to a workspace, so they are kept in the person's own scope and
31/// stay on this device until their personal store carries them.
32///
33/// It is a resource like the app's own: its edits are its actions, applied
34/// by the replayable executor, attributed to the person, written into its
35/// history and undone per step ([`Self::undo`]). It is created with the
36/// resource's genesis ([`Resource::initialize`]) where it does not exist
37/// yet. Read it through [`Self::state`] or [`Self::with`]; an edit from
38/// another window or device reaches it on the next frame.
39pub struct PersonRecord<P: Resource> {
40    shared: Rc<Shared<P>>,
41}
42
43impl<P: Resource> Clone for PersonRecord<P> {
44    fn clone(&self) -> Self {
45        Self {
46            shared: Rc::clone(&self.shared),
47        }
48    }
49}
50
51struct Shared<P: Resource> {
52    record: StoredRecord,
53    /// The app that keeps the record, when this app does not change it:
54    /// it is read here and its edits are refused.
55    owner: Option<&'static str>,
56    app_id: &'static str,
57    app_title: &'static str,
58    me: String,
59    /// The state, followed change by change: what views of the record read.
60    live: Live<P>,
61    /// The same state, for controls bound to it.
62    state: LocalState<Rc<P::State>>,
63    /// The record as views and the door edit it.
64    held: RefCell<Option<Rc<Held<P>>>>,
65}
66
67impl<P: Resource> Shared<P> {
68    /// Follow the record when it moved, reading only what changed. Whether
69    /// it did.
70    fn refresh(&self) -> bool {
71        let mut moved = match self.record.read(|doc| self.live.sync(doc)) {
72            Some(Ok(moved)) => moved,
73            Some(Err(error)) => {
74                unreadable_person_record(P::KIND, &error);
75                false
76            }
77            None => false,
78        };
79        if !moved && self.live.unreadable().is_none() {
80            // The record is small and read whole, so a change it cannot
81            // read is noticed here rather than by whoever publishes it. The
82            // reader reads it again only when the document moved since it
83            // last read it whole; what that read found is the state too.
84            match self.record.read(|doc| self.live.reread(doc)) {
85                Some(Ok(reread)) => moved = reread,
86                Some(Err(error)) => unreadable_person_record(P::KIND, &error),
87                None => {}
88            }
89        }
90        // Views of the record follow it whole: it is small.
91        let _ = self.live.take_change();
92        if moved {
93            self.state.set(self.live.get());
94        }
95        moved
96    }
97}
98
99impl<P: Resource> PersonRecord<P> {
100    /// The current state.
101    #[must_use]
102    pub fn get(&self) -> Rc<P::State> {
103        self.shared.refresh();
104        self.shared.state.with(Rc::clone)
105    }
106
107    /// Read the current state.
108    pub fn with<T>(&self, read: impl FnOnce(&P::State) -> T) -> T {
109        read(&self.get())
110    }
111
112    /// Why the record stopped reading as `P`, if it did: the state is then
113    /// the last one that read, and a view shows this instead of it. Its
114    /// published actions say the same.
115    #[must_use]
116    pub fn unreadable(&self) -> Option<String> {
117        self.shared.refresh();
118        self.shared.live.unreadable().map(|why| why.reason)
119    }
120
121    /// The state as view state that follows every change, for a control
122    /// bound to it.
123    #[must_use]
124    pub fn state(&self) -> LocalState<Rc<P::State>> {
125        self.shared.state.clone()
126    }
127
128    /// Increases whenever the state changes.
129    #[must_use]
130    pub fn revision(&self) -> u64 {
131        self.shared.refresh();
132        self.shared.live.revision()
133    }
134
135    /// Apply `action` now: one change of the record, attributed to the
136    /// person, recorded in its history and — for content the person made —
137    /// one undo step of theirs.
138    pub fn apply(&self, action: &P::Action) -> Result<(), ActionError> {
139        self.apply_as(action, &ActionInvoker::actor(&self.shared.me))
140    }
141
142    /// [`Self::apply`] by `invoker`: the person, or software acting for
143    /// them.
144    pub(crate) fn apply_as(
145        &self,
146        action: &P::Action,
147        invoker: &ActionInvoker,
148    ) -> Result<(), ActionError> {
149        let shared = &self.shared;
150        if let Some(owner) = shared.owner {
151            return Err(ActionError::Refused(format!(
152                "the person's {} are edited in {owner}",
153                P::LABEL.to_lowercase()
154            )));
155        }
156        let encoded = tilo_resource::encode::<P>(action)?;
157        let change = action.change_kind();
158        let context = ActionContext::for_resource(P::KIND, P::KIND, P::LABEL)
159            .with_app(shared.app_id, shared.app_title)
160            .with_invoker(invoker.clone())
161            .with_change(change);
162        shared
163            .record
164            .edit(change == ChangeKind::Content, |doc| {
165                let writer =
166                    ui_core::history_source::authors::device_actor(&shared.me, doc.get_actor());
167                tilo_actions::apply_local_with_context(doc, action, encoded, Some(&writer), context)
168                    .map(|_| ())
169                    .map_err(|error| error.to_string())
170            })
171            .map_err(ActionError::Refused)?;
172        shared.refresh();
173        Ok(())
174    }
175
176    /// Apply the action `invoke` names, as [`Self::apply`].
177    pub fn invoke(&self, invoke: &Invoke<P>) -> Result<(), ActionError> {
178        let action = invoke
179            .build(tilo_resource::origin_now_ms())
180            .map_err(ActionError::Refused)?;
181        self.apply(&action)
182    }
183
184    /// Whether this app changes the record: its own, or a shared one it is
185    /// an editor of; any other is read here.
186    #[must_use]
187    pub fn editable(&self) -> bool {
188        self.shared.owner.is_none()
189    }
190
191    /// Undo the person's last edit of the record. Whether there was one.
192    pub fn undo(&self) -> bool {
193        let undone = self.shared.record.undo();
194        self.shared.refresh();
195        undone
196    }
197
198    /// Redo the person's last undone edit of the record.
199    pub fn redo(&self) -> bool {
200        let redone = self.shared.record.redo();
201        self.shared.refresh();
202        redone
203    }
204
205    /// Whether the record reaches other devices: it has a route. One kept
206    /// on this device only has none.
207    #[must_use]
208    pub fn syncs(&self) -> bool {
209        self.shared.record.syncs()
210    }
211
212    /// Where the content this record references — a picture its state
213    /// names by hash — is stored and read: with the record, carried on its
214    /// route. `None` for a record kept on this device only, which keeps no
215    /// content.
216    #[must_use]
217    pub fn content(&self) -> Option<tilo_content::ContentStore> {
218        self.shared.record.content()
219    }
220
221    /// The record underneath, for a test or host that carries it between
222    /// devices itself.
223    #[must_use]
224    pub fn record(&self) -> &StoredRecord {
225        &self.shared.record
226    }
227}
228
229/// An open record: the app that owns it, and its kind.
230type OpenKey = (&'static str, String);
231
232/// The person's records a root's views opened, shared by every scope of the
233/// root: one [`PersonRecord`] per resource kind.
234pub(crate) struct PersonState {
235    /// The person's shared records ([`tilo_resource::SharedRecord`]); none
236    /// for a host that keeps only the settings sections, whose history is
237    /// the settings document's.
238    records: Option<PersonRecords>,
239    /// The app's own records of a kind, in the workspace it is open in;
240    /// none for a host that keeps only the shared ones (the session's
241    /// settings).
242    own: Option<PersonRecords>,
243    app_id: &'static str,
244    app_title: &'static str,
245    me: String,
246    /// The ids they went by before this device had an identity, under which
247    /// what they changed is still theirs.
248    earlier: Vec<String>,
249    /// The open records, by the app that owns them and their kind.
250    opened: RefCell<HashMap<OpenKey, Rc<dyn Any>>>,
251    /// The records of the person's other apps keep, held here too.
252    others: RefCell<HashMap<&'static str, PersonRecords>>,
253    owners: Rc<RefCell<Vec<HistoryOwner>>>,
254    refreshers: RefCell<Vec<Box<dyn Fn() -> bool>>>,
255    /// The open records' undo and redo, as the palette and agents reach
256    /// them.
257    published: RefCell<Vec<Published>>,
258    /// The records that could not be opened as views asked for them, by
259    /// what they are, with what they publish: their declared actions,
260    /// unavailable for why. Those views are told now, and never again for
261    /// the same record.
262    unavailable: RefCell<HashMap<(String, &'static str), tilo_actions::ResourceSpec>>,
263}
264
265/// One open record as the root publishes it.
266struct Published {
267    id: String,
268    label: String,
269    /// Its field edits and verbs, as the door reaches them.
270    door: Rc<dyn RecordDoor>,
271    /// Take back (`true`) or redo (`false`) one of the person's steps, for
272    /// the invoker named. Whether there was one.
273    step: Step,
274}
275
276/// Take back (`true`) or redo (`false`) one step for the invoker named.
277type Step = Box<dyn Fn(bool, &str) -> bool>;
278
279/// A person record just opened as its type: its shared state, its door and
280/// its undo and redo.
281struct Opened {
282    shared: Rc<dyn Any>,
283    door: Rc<dyn RecordDoor>,
284    step: Step,
285}
286
287/// The operation type of a person record's undo and redo.
288pub(crate) const HISTORY_TYPE: &str = "tilo.app.person-history";
289
290/// The most steps one undo or redo of a person record takes.
291const MAX_STEPS: i64 = 50;
292
293/// What a person record's undo or redo was asked.
294#[derive(serde::Serialize, serde::Deserialize)]
295struct HistoryRequest {
296    record: String,
297    undo: bool,
298    steps: Option<i64>,
299}
300
301/// The address a person record of `kind` is published under.
302#[must_use]
303pub(crate) fn resource_id(kind: &str) -> String {
304    format!("person:{kind}")
305}
306
307/// One of the person's own records an app keeps, declared with the app
308/// ([`ResourceApp::PERSON_RECORDS`](super::ResourceApp::PERSON_RECORDS)).
309///
310/// It is the person's, not the workspace's: kept in their own scope, carried
311/// to their other devices by their personal store, and never reaching a
312/// collaborator. The app's committed surface names it whether or not a view
313/// of it is showing, so the host admits the app's changes to it — and its
314/// reads of it — against the surface.
315#[derive(Clone, Copy)]
316pub struct PersonKind {
317    kind: &'static str,
318    report: fn() -> tilo_resource::Surface,
319    open: fn(&super::settings::ViewStorage),
320}
321
322impl PersonKind {
323    /// The person's record `P`, kept by the app that owns it
324    /// ([`SharedRecord`](tilo_resource::SharedRecord)).
325    #[must_use]
326    pub const fn shared<P: tilo_resource::SharedRecord>() -> Self {
327        Self {
328            kind: P::KIND,
329            report: shared_surface::<P>,
330            open: open_shared::<P>,
331        }
332    }
333
334    /// The kind of record it is.
335    #[must_use]
336    pub const fn kind(&self) -> &'static str {
337        self.kind
338    }
339
340    /// Its surface, as the app's surface report lists it: `P`'s, marked as
341    /// the person's own.
342    #[must_use]
343    pub fn surface(&self) -> tilo_resource::Surface {
344        (self.report)()
345    }
346
347    /// Open it with the window, so it is published before any view shows it.
348    pub(crate) fn open(&self, storage: &super::settings::ViewStorage) {
349        (self.open)(storage);
350    }
351}
352
353fn shared_surface<P: tilo_resource::SharedRecord>() -> tilo_resource::Surface {
354    tilo_resource::Surface {
355        person: true,
356        ..P::surface()
357    }
358}
359
360fn open_shared<P: tilo_resource::SharedRecord>(storage: &super::settings::ViewStorage) {
361    let _ = storage.shared_person::<P>();
362}
363
364fn history_spec(record: &str, label: &str, undo: bool) -> tilo_actions::ActionSpec {
365    let mut spec = history_operation(record.to_owned(), label, undo);
366    spec.type_mut().contract.result = tilo_actions::ResultSpec::Json {
367        schema: serde_json::json!({
368            "type": "object",
369            "properties": { "steps": { "type": "integer" } },
370        }),
371    };
372    spec
373}
374
375fn history_operation(record: String, label: &str, undo: bool) -> tilo_actions::ActionSpec {
376    let (id, verb) = if undo {
377        ("undo", "Undo")
378    } else {
379        ("redo", "Redo")
380    };
381    tilo_actions::ActionSpec::typed_operation(
382        id,
383        verb,
384        verb,
385        tilo_actions::OperationKind::Effect,
386        HISTORY_TYPE,
387        vec![
388            tilo_actions::ActionParam::optional(
389                "steps",
390                tilo_actions::ParamType::Number,
391                "How many of your steps (default 1, at most 50)",
392            )
393            .nullable(),
394        ],
395        move |args| {
396            tilo_actions::EncodedAction::encode_operation(
397                HISTORY_TYPE,
398                &HistoryRequest {
399                    record: record.clone(),
400                    undo,
401                    steps: args.integer("steps"),
402                },
403            )
404            .unwrap_or_else(|_| tilo_actions::EncodedAction {
405                type_id: tilo_actions::resource::INVALID_ARGUMENTS_TYPE.to_owned(),
406                payload: Vec::new(),
407            })
408        },
409    )
410    .of_kind(tilo_actions::ActionKind::Read)
411    .with_description(if undo {
412        format!("Take back your last change to your {label}.")
413    } else {
414        format!("Make again the change to your {label} you last took back.")
415    })
416}
417
418impl PersonState {
419    pub(crate) fn new(
420        records: PersonRecords,
421        own: Option<PersonRecords>,
422        app_id: &'static str,
423        app_title: &'static str,
424    ) -> Self {
425        let me = records.author().0.clone();
426        Self::named(Some(records), own, me, Vec::new(), app_id, app_title)
427    }
428
429    /// The person `me`, whose shared records this host does not hold: their
430    /// undo and redo of the settings sections are published all the same,
431    /// and read the sections' history in the settings document. `earlier`
432    /// are the ids they went by before this device had an identity, whose
433    /// changes are theirs too.
434    pub(crate) fn without_records(
435        me: String,
436        earlier: Vec<String>,
437        app_id: &'static str,
438        app_title: &'static str,
439    ) -> Self {
440        Self::named(None, None, me, earlier, app_id, app_title)
441    }
442
443    fn named(
444        records: Option<PersonRecords>,
445        own: Option<PersonRecords>,
446        me: String,
447        earlier: Vec<String>,
448        app_id: &'static str,
449        app_title: &'static str,
450    ) -> Self {
451        Self {
452            records,
453            own,
454            app_id,
455            app_title,
456            me,
457            earlier,
458            opened: RefCell::default(),
459            others: RefCell::default(),
460            owners: Rc::default(),
461            refreshers: RefCell::default(),
462            published: RefCell::default(),
463            unavailable: RefCell::default(),
464        }
465    }
466
467    /// The person's record of `P`, opened on first use.
468    pub(crate) fn open<P: Resource>(&self) -> Option<PersonRecord<P>> {
469        self.open_of::<P>(self.app_id, true, true, false)
470    }
471
472    /// The shared record `P`, opened on first use: kept by its owner, a copy
473    /// of it here in every other app, changed where `P` says.
474    pub(crate) fn open_shared<P: tilo_resource::SharedRecord>(&self) -> Option<PersonRecord<P>> {
475        self.open_of::<P>(P::OWNER, P::edited_by(self.app_id), true, true)
476    }
477
478    /// The shared record `P`, opened on first use as [`Self::open_shared`]
479    /// but never published: a host holds it for its own edits, and the app
480    /// whose window shows it publishes its actions.
481    pub(crate) fn hold_shared<P: tilo_resource::SharedRecord>(&self) -> Option<PersonRecord<P>> {
482        self.open_of::<P>(P::OWNER, P::edited_by(self.app_id), false, true)
483    }
484
485    /// The record of `P` that the app `owner` keeps, opened on first use,
486    /// whether this app changes it, whether it is published when it is
487    /// this app's own, and whether it is one of the person's shared records
488    /// rather than the app's own in its workspace.
489    fn open_of<P: Resource>(
490        &self,
491        owner: &'static str,
492        edits: bool,
493        publish: bool,
494        shared: bool,
495    ) -> Option<PersonRecord<P>> {
496        let open = |record: StoredRecord| -> Result<Opened, String> {
497            let shared = self.shared_of::<P>(record, (!edits).then_some(owner))?;
498            let held = Rc::new(held::<P>(&shared, edits));
499            *shared.held.borrow_mut() = Some(Rc::clone(&held));
500            let weak = Rc::downgrade(&shared);
501            Ok(Opened {
502                shared: shared as Rc<dyn Any>,
503                door: held,
504                step: Box::new(move |undo, _| {
505                    weak.upgrade().is_some_and(|shared| {
506                        let record = PersonRecord { shared };
507                        if undo { record.undo() } else { record.redo() }
508                    })
509                }),
510            })
511        };
512        let shared = self.open_kind(
513            owner,
514            &RecordType {
515                kind: P::KIND,
516                label: P::LABEL,
517                unreadable: &|reason| {
518                    tilo_resource::resource::unreadable_parts::<P>(
519                        &resource_id(P::KIND),
520                        P::LABEL,
521                        reason,
522                    )
523                },
524            },
525            &|doc| P::initialize(doc).map_err(|error| error.to_string()),
526            &open,
527            (publish, shared),
528        )?;
529        shared
530            .downcast::<Shared<P>>()
531            .ok()
532            .map(|shared| PersonRecord { shared })
533    }
534
535    /// The open record of `kind` that the app `owner` keeps, or the one
536    /// `open` makes of it, created by `initialize` where it does not exist
537    /// yet, kept and (for this app's own, when `publish`) published: the
538    /// part of opening a record that does not depend on its type. A
539    /// `shared` one is the person's ([`tilo_resource::SharedRecord`]); any
540    /// other is the app's own, in the workspace it is open in.
541    fn open_kind(
542        &self,
543        owner: &'static str,
544        of: &RecordType<'_>,
545        initialize: &dyn Fn(&mut ui_core::automerge::AutoCommit) -> Result<(), String>,
546        open: &dyn Fn(StoredRecord) -> Result<Opened, String>,
547        (publish, shared): (bool, bool),
548    ) -> Option<Rc<dyn Any>> {
549        let (kind, label) = (of.kind, of.label);
550        let key = (owner, kind.to_owned());
551        if let Some(open) = self.opened.borrow().get(&key) {
552            return Some(Rc::clone(open));
553        }
554        let records = if shared {
555            self.records_of(owner)?
556        } else {
557            self.own.clone()?
558        };
559        let record = match open_record(&records, kind.to_owned(), kind, initialize) {
560            Ok(record) => record,
561            Err(reason) => return self.note_unavailable(owner, of, &reason, publish),
562        };
563        let Opened { shared, door, step } = match open(record) {
564            Ok(opened) => opened,
565            Err(reason) => return self.note_unavailable(owner, of, &reason, publish),
566        };
567        if owner != self.app_id || !publish {
568            self.opened.borrow_mut().insert(key, Rc::clone(&shared));
569            return Some(shared);
570        }
571        self.publish(
572            key,
573            Rc::clone(&shared),
574            Published {
575                id: resource_id(kind),
576                label: label.to_owned(),
577                door,
578                step,
579            },
580        );
581        Some(shared)
582    }
583
584    /// `record` read as `P`, followed from now on.
585    ///
586    /// # Errors
587    /// Why the record does not read as `P`: it could not be migrated to the
588    /// storage version the app reads, or what it holds is not a `P`.
589    fn shared_of<P: Resource>(
590        &self,
591        record: StoredRecord,
592        owner: Option<&'static str>,
593    ) -> Result<Rc<Shared<P>>, String> {
594        // An older record is brought up to the resource's storage version
595        // before it is read, as the app's record is when it opens: once, as
596        // the same housekeeping change on every device, nobody's undo step.
597        let older = record
598            .read(tilo_resource::evolution::needs_migrate::<P>)
599            .ok_or_else(|| "the record cannot be read".to_owned())?;
600        if older
601            && let Err(error) = record.edit(false, |doc| {
602                tilo_resource::evolution::migrate::<P>(doc).map_err(|error| error.to_string())
603            })
604        {
605            return Err(format!(
606                "it could not be brought up to this app's storage version: {error}"
607            ));
608        }
609        let reader = record
610            .read(tilo_resource::Reader::<P>::read)
611            .ok_or_else(|| "the record cannot be read".to_owned())?
612            .map_err(|error| format!("it does not read: {error}"))?;
613        let live = Live::new(reader);
614        self.owners.borrow_mut().push(record.history_owner());
615        let shared = Rc::new(Shared::<P> {
616            record,
617            owner,
618            app_id: self.app_id,
619            app_title: self.app_title,
620            me: self.me.clone(),
621            state: LocalState::new(live.get()),
622            live,
623            held: RefCell::new(None),
624        });
625        let weak = Rc::downgrade(&shared);
626        self.refreshers.borrow_mut().push(Box::new(move || {
627            weak.upgrade().is_some_and(|shared| shared.refresh())
628        }));
629        Ok(shared)
630    }
631
632    /// The settings section `kind` held as a record, opened on first use and
633    /// published with the undo and redo of each invoker's steps on it.
634    pub(crate) fn open_section(&self, kind: Rc<SectionKind>) -> Rc<SectionDoor> {
635        let key = (self.app_id, kind.kind.to_owned());
636        if let Some(door) = self
637            .opened
638            .borrow()
639            .get(&key)
640            .and_then(|open| Rc::clone(open).downcast::<SectionDoor>().ok())
641        {
642            return door;
643        }
644        let door = Rc::new(SectionDoor::new(
645            kind,
646            self.me.clone(),
647            self.earlier.clone(),
648        ));
649        let stepping = Rc::downgrade(&door);
650        self.publish(
651            key,
652            Rc::clone(&door) as Rc<dyn Any>,
653            Published {
654                id: door.id().to_owned(),
655                label: door.label().to_owned(),
656                door: Rc::clone(&door) as Rc<dyn RecordDoor>,
657                step: Box::new(move |undo, by| {
658                    stepping.upgrade().is_some_and(|door| door.step(undo, by))
659                }),
660            },
661        );
662        door
663    }
664
665    /// The person's records `owner` keeps: this app's own, or another app's,
666    /// opened once.
667    fn records_of(&self, owner: &'static str) -> Option<PersonRecords> {
668        // A host that keeps the person's shared records lends this app its
669        // own as well: a second writer of them here would store nowhere the
670        // host stores or syncs.
671        if owner == self.app_id && !self.records.as_ref()?.shared_by_host() {
672            return self.records.clone();
673        }
674        let mut others = self.others.borrow_mut();
675        if let Some(records) = others.get(owner) {
676            return Some(records.clone());
677        }
678        let records = self
679            .records
680            .as_ref()?
681            .of_package(owner)
682            .map_err(|error| {
683                tracing::warn!(owner, %error, "the person's records of another app are unavailable");
684            })
685            .ok()?;
686        others.insert(owner, records.clone());
687        Some(records)
688    }
689
690    /// Note that the record `P` that `owner` keeps could not be opened, for
691    /// `reason`: the root publishes it with its declared actions
692    /// unavailable for that reason, rather than the record going quietly
693    /// missing from the surface.
694    fn note_unavailable(
695        &self,
696        owner: &'static str,
697        of: &RecordType<'_>,
698        reason: &str,
699        publish: bool,
700    ) -> Option<Rc<dyn Any>> {
701        unreadable_person_record(of.kind, &reason);
702        // Only the record this app would have published is published for
703        // the reason: one another app keeps and this one reads is that
704        // app's surface, which registers the actions its own record has.
705        if owner == self.app_id && publish {
706            let published = (of.unreadable)(reason);
707            self.unavailable
708                .borrow_mut()
709                .insert((owner.to_owned(), of.kind), published);
710        }
711        None
712    }
713
714    /// Every record a view asked for that could not be opened, as it is
715    /// published: its declared actions, unavailable for why.
716    pub(crate) fn unavailable(&self) -> Vec<tilo_actions::ResourceSpec> {
717        self.unavailable.borrow().values().cloned().collect()
718    }
719
720    /// Keep the record opened under `key`, publishing its door and its undo
721    /// and redo.
722    fn publish(&self, key: OpenKey, open: Rc<dyn Any>, published: Published) {
723        self.published.borrow_mut().push(published);
724        self.opened.borrow_mut().insert(key, open);
725    }
726
727    /// Read every open record that moved. Whether one did.
728    pub(crate) fn refresh(&self) -> bool {
729        self.refreshers
730            .borrow()
731            .iter()
732            .fold(false, |moved, refresh| refresh() | moved)
733    }
734
735    /// The open records as resources of their own: the person's undo and
736    /// redo of each, which the palette and agents reach like the app's own
737    /// history.
738    pub(crate) fn resources(&self) -> Vec<tilo_actions::ResourceSpec> {
739        self.published
740            .borrow()
741            .iter()
742            .map(|published| {
743                published
744                    .door
745                    .publish()
746                    .with_action(history_spec(&published.id, &published.label, true))
747                    .with_action(history_spec(&published.id, &published.label, false))
748            })
749            .collect()
750    }
751
752    /// The open records of the person's own, as the door changes them.
753    pub(crate) fn doors(&self) -> Vec<Rc<dyn RecordDoor>> {
754        self.published
755            .borrow()
756            .iter()
757            .map(|published| Rc::clone(&published.door))
758            .collect()
759    }
760
761    pub(crate) fn me(&self) -> &str {
762        &self.me
763    }
764
765    /// Answer an undo or redo of an open record: how many steps it took.
766    pub(crate) fn answer(&self, payload: &[u8]) -> Result<tilo_actions::ActionOutput, String> {
767        let invoker = ui_core::invocation_scope::current_invoker()
768            .unwrap_or_else(|| ActionInvoker::actor(&self.me));
769        self.answer_for(payload, &invoker)
770    }
771
772    /// [`Self::answer`] for `invoker`.
773    pub(crate) fn answer_for(
774        &self,
775        payload: &[u8],
776        invoker: &ActionInvoker,
777    ) -> Result<tilo_actions::ActionOutput, String> {
778        let request: HistoryRequest =
779            tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
780        let steps = request.steps.unwrap_or(1);
781        if !(1..=MAX_STEPS).contains(&steps) {
782            return Err(format!("steps is 1 to {MAX_STEPS}"));
783        }
784        if invoker.actor != self.me && invoker.on_behalf_of.as_deref() != Some(self.me.as_str()) {
785            return Err(format!(
786                "`{}` is {}'s own; only they change it",
787                request.record, self.me
788            ));
789        }
790        let published = self.published.borrow();
791        let record = published
792            .iter()
793            .find(|published| published.id == request.record)
794            .ok_or_else(|| format!("no record of yours is open as {}", request.record))?;
795        let mut taken = 0;
796        while taken < steps && (record.step)(request.undo, &invoker.actor) {
797            taken = taken.saturating_add(1);
798        }
799        if taken == 0 {
800            return Err(if request.undo {
801                "nothing of yours to undo".to_owned()
802            } else {
803                "nothing of yours to redo".to_owned()
804            });
805        }
806        Ok(tilo_actions::ActionOutput {
807            value: Some(serde_json::json!({ "steps": taken })),
808            resources: vec![record.id.clone()],
809        })
810    }
811
812    /// The routes of the open records, which the runtime syncs: a record
813    /// kept on this device only has none.
814    pub(crate) fn owners(&self) -> Vec<HistoryOwner> {
815        self.owners.borrow().clone()
816    }
817}
818
819/// The record `kind` that `records` keeps, made by `initialize` where it
820/// does not exist yet.
821///
822/// # Errors
823/// Why the record cannot be opened: the store refused it, or it cannot be
824/// created.
825fn open_record(
826    records: &PersonRecords,
827    kind: String,
828    of: &str,
829    initialize: &dyn Fn(&mut ui_core::automerge::AutoCommit) -> Result<(), String>,
830) -> Result<StoredRecord, String> {
831    records
832        .open_record(RecordKind::new(kind), initialize)
833        .map_err(|error| format!("{of}'s record cannot be opened: {error}"))
834}
835
836/// The record `shared` as views and the door edit it: read into a state of
837/// its own that follows the record, changed as the person.
838fn held<P: Resource>(shared: &Rc<Shared<P>>, edits: bool) -> Held<P> {
839    let following = Rc::downgrade(shared);
840    let applying = Rc::downgrade(shared);
841    Held::new(
842        resource_id(P::KIND),
843        format!("Your {}", P::LABEL.to_lowercase()),
844        edits,
845        shared.live.clone(),
846        move |_: &Live<P>| {
847            if let Some(shared) = following.upgrade() {
848                shared.refresh();
849            }
850        },
851        move |action: &P::Action, invoker: &ActionInvoker| {
852            let shared = applying
853                .upgrade()
854                .ok_or_else(|| ActionError::Refused("the record is closed".into()))?;
855            PersonRecord { shared }.apply_as(action, invoker)
856        },
857    )
858}
859
860impl<P: Resource> super::record::sealed::Hold<P> for PersonRecord<P> {
861    fn held(&self, _storage: super::record::sealed::Storage<'_>) -> Option<Rc<Held<P>>> {
862        self.shared.held.borrow().clone()
863    }
864
865    fn door(
866        &self,
867        _storage: super::record::sealed::Storage<'_>,
868    ) -> Option<super::record::sealed::Door> {
869        self.shared
870            .held
871            .borrow()
872            .clone()
873            .map(|held| super::record::sealed::Door(held as Rc<dyn RecordDoor>))
874    }
875}
876
877impl Drop for PersonState {
878    /// The window closes: what the person did is stored before it goes.
879    fn drop(&mut self) {
880        for records in self.records.iter().chain(&self.own) {
881            if let Err(error) = records.flush() {
882                tracing::warn!(%error, "the records beside the app's own could not be stored");
883            }
884        }
885    }
886}
887
888/// Log that the person's record of `kind` does not read: one event site in
889/// the runtime rather than one in every app for each record type.
890#[cold]
891#[inline(never)]
892fn unreadable_person_record(kind: &'static str, error: &dyn std::fmt::Display) {
893    tracing::warn!(kind, %error, "person record unreadable");
894}
895
896/// A person record's type, as the part of opening one that does not depend
897/// on it reads it: compiled once, not into every app.
898struct RecordType<'a> {
899    kind: &'static str,
900    label: &'static str,
901    /// What the record publishes when it could not be opened, for why.
902    unreadable: &'a dyn Fn(&str) -> tilo_actions::ResourceSpec,
903}