Skip to main content

tilo_app/framework/
record.rs

1//! Records a view edits besides the one its app shows: the person's own
2//! record of a resource ([`PersonRecord`](super::PersonRecord)) and a typed
3//! settings section ([`Setting`](super::Setting)).
4//!
5//! Such a record is a resource like the app's own. A view binds its fields by
6//! path and its verbs by reference in a scope of its own
7//! ([`ViewCx::record`](super::ViewCx::record)); a surface binds its controls
8//! and live rows with the record's actions
9//! ([`SurfaceCx::record_actions`](super::SurfaceCx::record_actions)).
10//! It is published beside the app's resource with its field edits and verbs,
11//! so the palette, agents and scripts change it through the same door. Only
12//! the person whose record it is changes it (or software acting for them),
13//! whatever their role in the workspace the app shows, and every change is
14//! recorded in the record's own history: a person record's document history,
15//! the settings document's log of who changed which setting.
16
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20use serde::{Deserialize, Serialize};
21use tilo_actions::{ActionError, ActionInvoker, EncodedAction, OperationKind};
22use tilo_resource::{Fields, Resource};
23use ui_core::storage::SettingsSchema;
24
25use super::controls::Actions;
26use super::dispatch::Dispatcher;
27use super::effect::Effects;
28use super::form::Forms;
29use super::history::HistoryFeeds;
30use super::live::Live;
31use super::reactive::{Change, Updaters};
32use super::settings::ViewStorage;
33use super::surface::SurfaceCx;
34use super::view::{Bindings, ViewCx};
35use ui_core::widget::Element;
36
37/// The operation type a record's published actions are carried out under:
38/// the host applies the action to the record it names.
39pub(crate) const RECORD_ACTION_TYPE: &str = "tilo.app.record-action";
40
41/// One action of a record, as its published operation carries it.
42#[derive(Serialize, Deserialize)]
43pub(crate) struct RecordAction {
44    pub(crate) record: String,
45    pub(crate) action: EncodedAction,
46}
47
48type Apply<P> = Box<dyn Fn(&<P as Resource>::Action, &ActionInvoker) -> Result<(), ActionError>>;
49type Sync<P> = Box<dyn Fn(&Live<P>)>;
50
51/// A record held for the views that edit it: its state as they read it and
52/// how an action applies to it.
53pub struct Held<P: Resource> {
54    id: String,
55    label: String,
56    editable: bool,
57    live: Live<P>,
58    sync: Sync<P>,
59    apply: Apply<P>,
60    /// The last publication, at the record's revision and loads and the
61    /// locale's revision it was built at: published again only when one
62    /// moved.
63    published: RefCell<Option<(Published, tilo_actions::ResourceSpec)>>,
64}
65
66/// The record's revision and loads ([`Live::loads`]), the locale's
67/// revision, and why the record did not read.
68type Published = (u64, u64, u64, Option<String>);
69
70impl<P: Resource> Held<P> {
71    pub(crate) fn new(
72        id: String,
73        label: String,
74        editable: bool,
75        live: Live<P>,
76        sync: impl Fn(&Live<P>) + 'static,
77        apply: impl Fn(&P::Action, &ActionInvoker) -> Result<(), ActionError> + 'static,
78    ) -> Self {
79        Self {
80            id,
81            label,
82            editable,
83            live,
84            sync: Box::new(sync),
85            apply: Box::new(apply),
86            published: RefCell::new(None),
87        }
88    }
89
90    pub(crate) fn id(&self) -> &str {
91        &self.id
92    }
93
94    pub(crate) fn editable(&self) -> bool {
95        self.editable
96    }
97
98    /// Its state, as views read it.
99    pub(crate) fn live(&self) -> &Live<P> {
100        &self.live
101    }
102
103    /// Follow what changed the record: this window, another, another of
104    /// the person's devices.
105    pub(crate) fn sync(&self) {
106        (self.sync)(&self.live);
107        // The record's views follow it whole: it is small.
108        let _ = self.live.take_change();
109    }
110
111    /// Apply `action` for `invoker`.
112    pub(crate) fn apply(
113        &self,
114        action: &P::Action,
115        invoker: &ActionInvoker,
116    ) -> Result<(), ActionError> {
117        if !self.editable {
118            return Err(ActionError::Refused(format!(
119                "{} is changed in another app",
120                self.label
121            )));
122        }
123        (self.apply)(action, invoker)?;
124        self.sync();
125        Ok(())
126    }
127
128    /// The record as it is published: its field edits and verbs carried out
129    /// through [`RECORD_ACTION_TYPE`], open to the person whatever their
130    /// role in the workspace; nothing to change where it is read only.
131    pub(crate) fn publish(&self) -> tilo_actions::ResourceSpec {
132        self.sync();
133        // Reading it again changes no revision — the state stays the last
134        // one that read — so whether it reads is part of what the
135        // publication is for.
136        let revision = (
137            self.live.revision(),
138            self.live.loads(),
139            ui_core::i18n::I18nManager::revision(),
140            self.live.unreadable().map(|why| why.reason),
141        );
142        if let Some((at, published)) = self.published.borrow().as_ref()
143            && *at == revision
144        {
145            return published.clone();
146        }
147        let published = if let Some(state) = self.live.get_checked() {
148            tilo_resource::publish::<P>(&self.id, &self.label, &state)
149        } else {
150            // Read here, never meant to change here: say why its actions do
151            // not apply rather than publish a record that reads as empty.
152            let why = self
153                .live
154                .unreadable()
155                .map_or_else(|| "the record does not read".to_owned(), |why| why.reason);
156            tilo_resource::resource::unreadable_parts::<P>(&self.id, &self.label, &why)
157        };
158        let published = route(published, &self.id, self.editable);
159        *self.published.borrow_mut() = Some((revision, published.clone()));
160        published
161    }
162}
163
164/// `resource`'s document actions, and those of its children, carried out on
165/// the record `record`; nothing else it publishes is answered here.
166pub(crate) fn route(
167    mut resource: tilo_actions::ResourceSpec,
168    record: &str,
169    editable: bool,
170) -> tilo_actions::ResourceSpec {
171    resource.actions = std::mem::take(&mut resource.actions)
172        .into_iter()
173        .filter(|spec| editable && spec.kind == OperationKind::Document)
174        .map(|spec| route_spec(spec, record))
175        .collect();
176    resource.children = std::mem::take(&mut resource.children)
177        .into_iter()
178        .map(|child| route(child, record, editable))
179        .collect();
180    resource
181}
182
183/// The document action `spec` carried out on the record `record`.
184pub(crate) fn route_spec(spec: tilo_actions::ActionSpec, record: &str) -> tilo_actions::ActionSpec {
185    let mut spec = route_operation(spec, RECORD_ACTION_TYPE, record);
186    // The workspace's roles do not reach a record of the person's own: the host
187    // lets only its person change it. It keeps its kind, so a change Undo
188    // takes back still runs at once and offers Undo.
189    spec.type_mut().contract.set_own_record();
190    spec
191}
192
193/// The document action `spec` carried out as the operation `operation` on
194/// the record `record`, needing what it declares.
195pub(crate) fn route_operation(
196    spec: tilo_actions::ActionSpec,
197    operation: &'static str,
198    record: &str,
199) -> tilo_actions::ActionSpec {
200    let record = record.to_owned();
201    spec.routed(operation, move |action| {
202        let routed = RecordAction {
203            record: record.clone(),
204            action,
205        };
206        EncodedAction::encode_operation(operation, &routed).unwrap_or_else(|error| EncodedAction {
207            type_id: tilo_actions::resource::INVALID_ARGUMENTS_TYPE.to_owned(),
208            payload: error.to_string().into_bytes(),
209        })
210    })
211}
212
213/// A held record with its type erased, as the host publishes and answers
214/// it.
215pub(crate) trait RecordDoor {
216    fn id(&self) -> &str;
217    /// Whether this app changes the record; it is read here otherwise.
218    fn editable(&self) -> bool;
219    /// Increases whenever what the record holds changed, read now.
220    fn revision(&self) -> u64;
221    fn publish(&self) -> tilo_actions::ResourceSpec;
222    fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String>;
223    /// [`Self::apply_encoded`], answering its invoker with what it made.
224    fn apply_answering(
225        &self,
226        action: &EncodedAction,
227        invoker: &ActionInvoker,
228    ) -> Result<tilo_actions::ActionOutput, String> {
229        self.apply_encoded(action, invoker)
230            .map(|()| tilo_actions::ActionOutput::default())
231    }
232    /// [`Self::apply_answering`] in the window's pass over the record it
233    /// shows, lent as `live`: what a door that writes that record too (a
234    /// container listing a new child) writes it through.
235    fn apply_in(
236        &self,
237        action: &EncodedAction,
238        invoker: &ActionInvoker,
239        live: &mut ui_core::widget::Edits<'_>,
240    ) -> Result<tilo_actions::ActionOutput, String> {
241        let _ = live;
242        self.apply_answering(action, invoker)
243    }
244}
245
246impl<P: Resource> RecordDoor for Held<P> {
247    fn id(&self) -> &str {
248        &self.id
249    }
250
251    fn editable(&self) -> bool {
252        self.editable
253    }
254
255    fn revision(&self) -> u64 {
256        self.sync();
257        self.live.revision()
258    }
259
260    fn publish(&self) -> tilo_actions::ResourceSpec {
261        Held::publish(self)
262    }
263
264    fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String> {
265        if action.type_id != P::ACTION_TYPE {
266            return Err(format!(
267                "{} takes no `{}` action",
268                self.label, action.type_id
269            ));
270        }
271        let action: P::Action =
272            tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
273        self.apply(&action, invoker)
274            .map_err(|error| error.to_string())
275    }
276}
277
278/// Answer one published action of a record: apply it to the record it
279/// names, for the person whose record it is.
280pub(crate) fn answer(
281    doors: &[Rc<dyn RecordDoor>],
282    me: Option<&str>,
283    payload: &[u8],
284) -> Result<tilo_actions::ActionOutput, String> {
285    let request: RecordAction =
286        tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
287    let door = doors
288        .iter()
289        .find(|door| door.id() == request.record)
290        .ok_or_else(|| format!("no record `{}` is open here", request.record))?;
291    let me = me.ok_or("this window changes no record of anyone's")?;
292    let invoker =
293        ui_core::invocation_scope::current_invoker().unwrap_or_else(|| ActionInvoker::actor(me));
294    let theirs = invoker.actor == me || invoker.on_behalf_of.as_deref() == Some(me);
295    if !theirs {
296        return Err(format!(
297            "`{}` is {me}'s own; only they change it",
298            request.record
299        ));
300    }
301    door.apply_encoded(&request.action, &invoker)?;
302    Ok(tilo_actions::ActionOutput::default())
303}
304
305/// A record other than the app's own that views and surfaces edit: the
306/// person's own record of `P` ([`PersonRecord`](super::PersonRecord)), or
307/// a settings section `P` that is a resource model
308/// ([`Setting`](super::Setting)).
309pub trait OtherRecord<P: Resource>: sealed::Hold<P> {}
310
311impl<P: Resource, H: sealed::Hold<P>> OtherRecord<P> for H {}
312
313pub(crate) mod sealed {
314    use std::rc::Rc;
315
316    use tilo_resource::Resource;
317
318    /// How a record is held for the window's views.
319    pub trait Hold<P: Resource> {
320        /// The record as a view part over it edits it through its type.
321        fn held(&self, storage: Storage<'_>) -> Option<Rc<super::Held<P>>>;
322        /// The record as the door publishes and changes it.
323        fn door(&self, storage: Storage<'_>) -> Option<Door>;
324    }
325
326    /// A held record as the door publishes and changes it.
327    pub struct Door(pub(crate) Rc<dyn super::RecordDoor>);
328
329    /// The window's storage, as a record is held in it.
330    pub struct Storage<'a>(pub(crate) &'a super::ViewStorage);
331}
332
333/// The records an app keeps besides the one it shows, declared in
334/// [`ResourceApp::records`](super::ResourceApp::records): each is opened
335/// with the window and published beside the app's resource, so the palette
336/// and agents reach it whether or not a view has shown it, and the app's
337/// committed surface lists it. The person's own shared records are declared
338/// with the app instead
339/// ([`ResourceApp::PERSON_RECORDS`](super::ResourceApp::PERSON_RECORDS)).
340pub struct Records<'a> {
341    /// The window's storage, where they are opened; `None` while the app's
342    /// records are only declared ([`Self::declaring`]).
343    storage: Option<&'a ViewStorage>,
344    /// The app's own record, as `Live` of its resource: what the actions of
345    /// a chain kept beside it read where they are invoked.
346    base: Option<Rc<dyn std::any::Any>>,
347    /// The door of the app's own record, as the `Dispatcher` of its
348    /// resource: what a container of children it lists writes it through.
349    dispatch: Option<Rc<dyn std::any::Any>>,
350    /// The resource types of the chains declared, while declaring.
351    chains: Vec<tilo_resource::ResourceKind>,
352    /// Where a host without a window keeps the settings sections declared,
353    /// with the app they are the settings of ([`super::SettingsRecords`]).
354    sections: Option<Sections>,
355    /// The provider keeping the records being declared, inside
356    /// [`Self::kept_by`].
357    provider: Option<&'static str>,
358    /// What [`Self::kept_by`] declared, with the provider keeping it.
359    kept: Vec<(&'static str, Declare)>,
360}
361
362/// A declaration of records ([`ResourceApp::records`](super::ResourceApp::records)).
363pub type Declare = fn(&mut Records<'_>);
364
365/// Where a host without a window holds the settings sections declared.
366pub(crate) struct Sections {
367    /// The app whose settings they are.
368    pub(crate) app_id: &'static str,
369    /// The settings document they are stored in.
370    pub(crate) document: ui_core::storage::SettingsDocument,
371    /// The person they are changed by: each invoker's steps on them are
372    /// published to undo and redo beside them.
373    pub(crate) person: Option<Rc<super::person::PersonState>>,
374    pub(crate) doors: Vec<Rc<super::section::SectionDoor>>,
375}
376
377impl<'a> Records<'a> {
378    pub(crate) fn new(storage: &'a ViewStorage) -> Self {
379        Self {
380            storage: Some(storage),
381            base: None,
382            dispatch: None,
383            chains: Vec::new(),
384            sections: None,
385            provider: None,
386            kept: Vec::new(),
387        }
388    }
389
390    /// The records an app declares, opening none: what a host without a
391    /// window learns of them (the resource types of its chains' parts, so
392    /// it reads a part as the window does).
393    pub(crate) const fn declaring() -> Self {
394        Self {
395            storage: None,
396            base: None,
397            dispatch: None,
398            chains: Vec::new(),
399            sections: None,
400            provider: None,
401            kept: Vec::new(),
402        }
403    }
404
405    /// The settings sections an app declares, held where `sections` says;
406    /// nothing else it declares is opened.
407    pub(crate) const fn sections(sections: Sections) -> Self {
408        Self {
409            storage: None,
410            base: None,
411            dispatch: None,
412            chains: Vec::new(),
413            sections: Some(sections),
414            provider: None,
415            kept: Vec::new(),
416        }
417    }
418
419    /// The settings sections held, after [`Self::sections`].
420    pub(crate) fn into_sections(self) -> Vec<Rc<super::section::SectionDoor>> {
421        self.sections
422            .map(|sections| sections.doors)
423            .unwrap_or_default()
424    }
425
426    /// The resource types of the chains declared.
427    pub(crate) fn into_chains(self) -> Vec<tilo_resource::ResourceKind> {
428        self.chains
429    }
430
431    /// What was declared kept by another provider ([`Self::kept_by`]),
432    /// with the provider keeping each: what a host standing in for those
433    /// providers (the native test hosts do) serves.
434    #[must_use]
435    pub fn into_kept(self) -> Vec<(&'static str, Declare)> {
436        self.kept
437    }
438
439    /// The records `declare` declares, kept by the host as `provider`
440    /// (the session keeps the person's settings sections): the window holds
441    /// and shows them and its views bind their controls to them as usual,
442    /// but each change is `provider`'s action, invoked there through the
443    /// workspace's broker and admitted, run and recorded there as any
444    /// caller's. The window publishes none of them; `provider` does.
445    pub fn kept_by(&mut self, provider: &'static str, declare: Declare) -> &mut Self {
446        self.kept.push((provider, declare));
447        let outer = self.provider.replace(provider);
448        declare(self);
449        self.provider = outer;
450        self
451    }
452
453    pub(crate) fn with_base<R: Resource>(mut self, live: Live<R>, dispatch: Dispatcher<R>) -> Self {
454        self.base = Some(Rc::new(live));
455        self.dispatch = Some(Rc::new(dispatch));
456        self
457    }
458
459    /// The chain of `P` the app keeps beside its record
460    /// ([`Chain`](super::Chain)): its head is held and it is published
461    /// with the window.
462    pub fn chain<P: super::Chained>(&mut self) -> &mut Self {
463        let Some(storage) = self.storage else {
464            self.chains.push(tilo_resource::ResourceKind::of::<P>());
465            return self;
466        };
467        let Some(base) = self
468            .base
469            .clone()
470            .and_then(|base| base.downcast::<Live<P::Base>>().ok())
471        else {
472            tracing::warn!(
473                kind = P::KIND,
474                "a chain's parts read the app's own resource as their base"
475            );
476            return self;
477        };
478        let _ = storage.chain::<P>(move || base.get());
479        self
480    }
481
482    /// The children of `C` the app's record lists
483    /// ([`Children`](super::Children)): each a record of its own, held and
484    /// published with the window.
485    pub fn children<C: super::ChildRecord>(&mut self) -> &mut Self {
486        let Some(storage) = self.storage else {
487            return self;
488        };
489        let Some(parent) = self
490            .base
491            .clone()
492            .and_then(|base| base.downcast::<Live<C::Parent>>().ok())
493        else {
494            tracing::warn!(
495                kind = C::KIND,
496                "children read the app's own resource as their parent"
497            );
498            return self;
499        };
500        let children = storage.children::<C>(move || parent.get());
501        if let (Some(children), Some(door)) = (
502            children,
503            self.dispatch
504                .clone()
505                .and_then(|door| door.downcast::<Dispatcher<C::Parent>>().ok()),
506        ) {
507            children.write_parent_through((*door).clone());
508        }
509        self
510    }
511
512    /// The app's record of `P` ([`RecordEnv::person`](super::RecordEnv::person)).
513    pub fn person<P: Resource>(&mut self) -> &mut Self {
514        let Some(storage) = self.storage else {
515            return self;
516        };
517        let _ = storage.person::<P>();
518        self
519    }
520
521    /// The record `P` that every app of the workspace opens and this app keeps
522    /// (`collab_types::WORKSPACE_RECORDS`; the workspace's emoji are
523    /// Chat's): opened with the window, so the app's surface lists it and
524    /// every member's change to it is decided against that surface.
525    pub fn shared_workspace<P: Resource>(&mut self) -> &mut Self {
526        let Some(storage) = self.storage else {
527            // Declared, not opened: a host that reads the record without the
528            // window ([`super::record_index`]) reads it by its kind.
529            self.chains.push(tilo_resource::ResourceKind::of::<P>());
530            return self;
531        };
532        storage.workspace_record::<P>();
533        self
534    }
535
536    /// The settings section `T`, a resource model ([`RecordEnv::settings`](super::RecordEnv::settings)).
537    pub fn settings<T>(&mut self) -> &mut Self
538    where
539        T: Resource<State = T> + Fields + SettingsSchema,
540    {
541        // A setting that means nothing on this device is not published.
542        if !T::available_on(ui_core::storage::DeviceClass::current()) {
543            return self;
544        }
545        if let Some(sections) = &mut self.sections {
546            let settings = ui_core::storage::Settings::<T>::for_app(sections.app_id)
547                .in_document(sections.document.clone());
548            let kind = super::section::SectionKind::of(&settings);
549            sections.doors.push(match &sections.person {
550                Some(person) => person.open_section(kind),
551                None => Rc::new(super::section::SectionDoor::new(
552                    kind,
553                    String::new(),
554                    Vec::new(),
555                )),
556            });
557        }
558        let Some(storage) = self.storage else {
559            return self;
560        };
561        let settings = storage.storage().settings::<T>();
562        let door = storage.section(&settings);
563        if let Some(provider) = self.provider {
564            storage.keep_by(door.id(), provider);
565        }
566        self
567    }
568}
569
570impl<R: Resource, T: 'static> ViewCx<R, T> {
571    /// A part of the view over another record than the app's own — the
572    /// person's own record ([`RecordEnv::person`](super::RecordEnv::person)), a settings section
573    /// ([`RecordEnv::settings`](super::RecordEnv::settings)) — built like a view of that record: its
574    /// controls bind the record's paths
575    /// (`cx.checkbox("high-contrast", Appearance::paths().high_contrast)`)
576    /// and verbs, its edits go to that record as the person, recorded in its
577    /// history, and the part follows every change of it. `None` where the
578    /// record cannot be held (a view of a past version).
579    pub fn record<P: Resource>(
580        &self,
581        record: &impl OtherRecord<P>,
582        part: impl FnOnce(&mut ViewCx<P>) -> Element,
583    ) -> Option<Element> {
584        let held = record.held(sealed::Storage(&self.storage))?;
585        let dispatch = record_dispatch(&self.dispatch, &held);
586        let actions = record_actions(
587            self.dispatch.app_id(),
588            &self.storage,
589            self.alive.clone(),
590            Rc::clone(&held) as Rc<dyn RecordDoor>,
591        );
592        let mut cx = ViewCx::<P> {
593            env: super::env::RecordEnv {
594                live: held.live().clone(),
595                alive: self.alive.clone(),
596                dispatch,
597                saved: self.saved.clone(),
598                saves: self.saves.clone(),
599                run: self.run.clone(),
600                storage: Rc::clone(&self.storage),
601                effects: Effects::inert(),
602            },
603            focus: Rc::new(|state| Some(state)),
604            updaters: Updaters::new(),
605            gate: None,
606            action_bindings: actions,
607            item: None,
608            path: Some(Vec::new()),
609            controls: Rc::clone(&self.controls),
610            forms: Forms::default(),
611            stages: self.stages.clone(),
612            feeds: HistoryFeeds::default(),
613            bindings: Bindings::new(),
614        };
615        let element = part(&mut cx);
616        let updaters = cx.updaters;
617        let followed = Cell::new(held.live().revision());
618        self.storage.follow_with(self.alive.clone(), move || {
619            held.sync();
620            let revision = held.live().revision();
621            if followed.replace(revision) != revision {
622                updaters.run(&held.live().get(), &Change::Everything);
623            }
624        });
625        Some(element)
626    }
627}
628
629/// The published actions of `held`, kept current with the record while
630/// `alive` lives: what a record's view part binds, and a surface's own
631/// controls and live rows ([`Actions::operation`]).
632fn record_actions<P: Resource>(
633    app: &'static str,
634    storage: &ViewStorage,
635    alive: std::rc::Weak<()>,
636    door: Rc<dyn RecordDoor>,
637) -> Actions<P> {
638    let actions = Actions::new(app, Rc::new(door.publish())).routed_to(
639        door.id(),
640        door.editable(),
641        storage.kept_provider(door.id()),
642    );
643    follow_publication(storage, alive, door, actions.table());
644    actions
645}
646
647/// Keep `table` current with `door`'s publication while `alive` lives.
648fn follow_publication(
649    storage: &ViewStorage,
650    alive: std::rc::Weak<()>,
651    door: Rc<dyn RecordDoor>,
652    table: super::controls::ActionTable,
653) {
654    let followed = Cell::new(door.revision());
655    storage.follow_with(alive, move || {
656        let revision = door.revision();
657        if followed.replace(revision) != revision {
658            table.update(door.publish());
659        }
660    });
661}
662
663impl<R: Resource, T: 'static> ViewCx<R, T> {
664    /// The published actions of another record than the app's own, for
665    /// controls a view builds outside a part over that record
666    /// ([`Self::record`]): a hand widget bound to a settings section's field
667    /// edits. `None` where the record cannot be held.
668    pub fn record_actions<P: Resource>(&self, record: &impl OtherRecord<P>) -> Option<Actions<P>> {
669        let door = record.door(sealed::Storage(&self.storage))?.0;
670        Some(record_actions(
671            self.dispatch.app_id(),
672            &self.storage,
673            self.alive.clone(),
674            door,
675        ))
676    }
677}
678
679impl<R: Resource> SurfaceCx<R> {
680    /// The published actions of another record than the app's own — the
681    /// person's own record, a settings section: a hand surface binds its
682    /// controls to the record's verbs and field edits with them, as to its
683    /// own, submits through [`Actions`]' bindings, and offers them on its
684    /// live rows ([`Actions::operation`]). `None` where the record cannot be
685    /// held.
686    pub fn record_actions<P: Resource>(&self, record: &impl OtherRecord<P>) -> Option<Actions<P>> {
687        let door = record.door(sealed::Storage(&self.storage))?.0;
688        Some(record_actions(
689            self.dispatch.app_id(),
690            &self.storage,
691            self.alive.clone(),
692            door,
693        ))
694    }
695}
696
697/// `parent`'s door, submitting to the held record `held`: what applies now
698/// applies to it as the person.
699fn record_dispatch<R: Resource, P: Resource>(
700    parent: &Dispatcher<R>,
701    held: &Rc<Held<P>>,
702) -> Dispatcher<P> {
703    let applying = Rc::downgrade(held);
704    let me = parent.me().map(str::to_owned);
705    parent.for_record(held.id(), !held.editable(), move |action| {
706        let held = applying
707            .upgrade()
708            .ok_or_else(|| ActionError::Refused("the record is closed".into()))?;
709        let me = me
710            .as_deref()
711            .ok_or_else(|| ActionError::Refused("nobody edits a past version".into()))?;
712        held.apply(action, &ActionInvoker::actor(me))
713    })
714}