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 room 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::{
22    ActionError, ActionInvoker, ActionKind, ActionNeed, EncodedAction, OperationKind,
23};
24use tilo_resource::{Fields, Resource};
25use ui_core::storage::SettingsSchema;
26
27use super::controls::Actions;
28use super::dispatch::Dispatcher;
29use super::effect::Effects;
30use super::form::Forms;
31use super::history::HistoryFeeds;
32use super::live::Live;
33use super::reactive::{Change, Updaters};
34use super::settings::ViewStorage;
35use super::surface::SurfaceCx;
36use super::view::{Bindings, ViewCx};
37use ui_core::widget::Element;
38
39/// The operation type a record's published actions are carried out under:
40/// the host applies the action to the record it names.
41pub(crate) const RECORD_ACTION_TYPE: &str = "tilo.app.record-action";
42
43/// One action of a record, as its published operation carries it.
44#[derive(Serialize, Deserialize)]
45struct RecordAction {
46    record: String,
47    action: EncodedAction,
48}
49
50type Apply<P> = Box<dyn Fn(&<P as Resource>::Action, &ActionInvoker) -> Result<(), ActionError>>;
51type Sync<P> = Box<dyn Fn(&Live<P>)>;
52
53/// A record held for the views that edit it: its state as they read it and
54/// how an action applies to it.
55pub struct Held<P: Resource> {
56    id: String,
57    label: String,
58    editable: bool,
59    live: Live<P>,
60    sync: Sync<P>,
61    apply: Apply<P>,
62    /// The last publication, at the record's and the locale's revision it
63    /// was built at: published again only when either moved.
64    published: RefCell<Option<((u64, u64), tilo_actions::Resource)>>,
65}
66
67impl<P: Resource> Held<P> {
68    pub(crate) fn new(
69        id: String,
70        label: String,
71        editable: bool,
72        live: Live<P>,
73        sync: impl Fn(&Live<P>) + 'static,
74        apply: impl Fn(&P::Action, &ActionInvoker) -> Result<(), ActionError> + 'static,
75    ) -> Self {
76        Self {
77            id,
78            label,
79            editable,
80            live,
81            sync: Box::new(sync),
82            apply: Box::new(apply),
83            published: RefCell::new(None),
84        }
85    }
86
87    /// The id the record is published under.
88    pub(crate) fn id(&self) -> &str {
89        &self.id
90    }
91
92    pub(crate) fn editable(&self) -> bool {
93        self.editable
94    }
95
96    /// Its state, as views read it.
97    pub(crate) fn live(&self) -> &Live<P> {
98        &self.live
99    }
100
101    /// Follow what changed the record: this window, another, another of
102    /// the person's devices.
103    pub(crate) fn sync(&self) {
104        (self.sync)(&self.live);
105        // The record's views follow it whole: it is small.
106        let _ = self.live.take_change();
107    }
108
109    /// Apply `action` for `invoker`.
110    ///
111    /// # Errors
112    /// The record is read here only, or the action refused.
113    pub(crate) fn apply(
114        &self,
115        action: &P::Action,
116        invoker: &ActionInvoker,
117    ) -> Result<(), ActionError> {
118        if !self.editable {
119            return Err(ActionError::Refused(format!(
120                "{} is changed in another app",
121                self.label
122            )));
123        }
124        (self.apply)(action, invoker)?;
125        self.sync();
126        Ok(())
127    }
128
129    /// The record as it is published: its field edits and verbs carried out
130    /// through [`RECORD_ACTION_TYPE`], open to the person whatever their
131    /// role in the room; nothing to change where it is read only.
132    pub(crate) fn publish(&self) -> tilo_actions::Resource {
133        self.sync();
134        let revision = (self.live.revision(), ui_core::i18n::I18nManager::revision());
135        if let Some((at, published)) = self.published.borrow().as_ref()
136            && *at == revision
137        {
138            return published.clone();
139        }
140        let state = self.live.get();
141        let published = route(
142            tilo_resource::publish::<P>(&self.id, &self.label, &state),
143            &self.id,
144            self.editable,
145        );
146        *self.published.borrow_mut() = Some((revision, published.clone()));
147        published
148    }
149}
150
151/// `resource`'s document actions, and those of its children, carried out on
152/// the record `record`; nothing else it publishes is answered here.
153pub(crate) fn route(
154    mut resource: tilo_actions::Resource,
155    record: &str,
156    editable: bool,
157) -> tilo_actions::Resource {
158    resource.actions = std::mem::take(&mut resource.actions)
159        .into_iter()
160        .filter(|spec| editable && spec.kind == OperationKind::Document)
161        .map(|spec| route_spec(spec, record))
162        .collect();
163    resource.children = std::mem::take(&mut resource.children)
164        .into_iter()
165        .map(|child| route(child, record, editable))
166        .collect();
167    resource
168}
169
170/// The document action `spec` carried out on the record `record`.
171pub(crate) fn route_spec(spec: tilo_actions::ActionSpec, record: &str) -> tilo_actions::ActionSpec {
172    let record = record.to_owned();
173    spec.routed(RECORD_ACTION_TYPE, move |action| {
174        let routed = RecordAction {
175            record: record.clone(),
176            action,
177        };
178        EncodedAction::encode_operation(RECORD_ACTION_TYPE, &routed).unwrap_or_else(|error| {
179            EncodedAction {
180                type_id: tilo_actions::resource::INVALID_ARGUMENTS_TYPE.to_owned(),
181                payload: error.to_string().into_bytes(),
182            }
183        })
184    })
185    // The room's roles do not reach a record of the person's own: the host
186    // lets only its person change it.
187    .of_kind(ActionKind::Read)
188    .with_need_of(|_| ActionNeed::of(ActionKind::Read))
189}
190
191/// A held record with its type erased, as the host publishes and answers
192/// it.
193pub(crate) trait RecordDoor {
194    fn id(&self) -> &str;
195    /// Whether this app changes the record; it is read here otherwise.
196    fn editable(&self) -> bool;
197    /// Increases whenever what the record holds changed, read now.
198    fn revision(&self) -> u64;
199    fn publish(&self) -> tilo_actions::Resource;
200    fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String>;
201}
202
203impl<P: Resource> RecordDoor for Held<P> {
204    fn id(&self) -> &str {
205        &self.id
206    }
207
208    fn editable(&self) -> bool {
209        self.editable
210    }
211
212    fn revision(&self) -> u64 {
213        self.sync();
214        self.live.revision()
215    }
216
217    fn publish(&self) -> tilo_actions::Resource {
218        Held::publish(self)
219    }
220
221    fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String> {
222        if action.type_id != P::ACTION_TYPE {
223            return Err(format!(
224                "{} takes no `{}` action",
225                self.label, action.type_id
226            ));
227        }
228        let action: P::Action =
229            tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
230        self.apply(&action, invoker)
231            .map_err(|error| error.to_string())
232    }
233}
234
235/// Answer one published action of a record: apply it to the record it
236/// names, for the person whose record it is.
237///
238/// # Errors
239/// The payload names no record of this window, someone else invoked it, or
240/// the action refused.
241pub(crate) fn answer(
242    doors: &[Rc<dyn RecordDoor>],
243    me: Option<&str>,
244    payload: &[u8],
245) -> Result<tilo_actions::ActionOutput, String> {
246    let request: RecordAction =
247        tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
248    let door = doors
249        .iter()
250        .find(|door| door.id() == request.record)
251        .ok_or_else(|| format!("no record `{}` is open here", request.record))?;
252    let me = me.ok_or("this window changes no record of anyone's")?;
253    let invoker =
254        ui_core::invocation_scope::current_invoker().unwrap_or_else(|| ActionInvoker::actor(me));
255    let theirs = invoker.actor == me || invoker.on_behalf_of.as_deref() == Some(me);
256    if !theirs {
257        return Err(format!(
258            "`{}` is {me}'s own; only they change it",
259            request.record
260        ));
261    }
262    door.apply_encoded(&request.action, &invoker)?;
263    Ok(tilo_actions::ActionOutput::default())
264}
265
266/// A record other than the app's own that views and surfaces edit: the
267/// person's own record of `P` ([`PersonRecord`](super::PersonRecord)), or
268/// a settings section `P` that is a resource model
269/// ([`Setting`](super::Setting)).
270pub trait OtherRecord<P: Resource>: sealed::Hold<P> {}
271
272impl<P: Resource, H: sealed::Hold<P>> OtherRecord<P> for H {}
273
274pub(crate) mod sealed {
275    use std::rc::Rc;
276
277    use tilo_resource::Resource;
278
279    /// How a record is held for the window's views.
280    pub trait Hold<P: Resource> {
281        /// The record as a view part over it edits it through its type.
282        fn held(&self, storage: Storage<'_>) -> Option<Rc<super::Held<P>>>;
283        /// The record as the door publishes and changes it.
284        fn door(&self, storage: Storage<'_>) -> Option<Door>;
285    }
286
287    /// A held record as the door publishes and changes it.
288    pub struct Door(pub(crate) Rc<dyn super::RecordDoor>);
289
290    /// The window's storage, as a record is held in it.
291    pub struct Storage<'a>(pub(crate) &'a super::ViewStorage);
292}
293
294/// The records an app keeps besides the one it shows, declared in
295/// [`ResourceApp::records`](super::ResourceApp::records): each is opened
296/// with the window and published beside the app's resource, so the palette
297/// and agents reach it whether or not a view has shown it, and the app's
298/// committed surface lists it.
299pub struct Records<'a> {
300    storage: &'a ViewStorage,
301}
302
303impl<'a> Records<'a> {
304    pub(crate) fn new(storage: &'a ViewStorage) -> Self {
305        Self { storage }
306    }
307
308    /// The person's own record of `P` ([`RecordEnv::person`](super::RecordEnv::person)).
309    pub fn person<P: Resource>(&mut self) -> &mut Self {
310        let _ = self.storage.person::<P>();
311        self
312    }
313
314    /// The person's record `P` another app keeps
315    /// ([`RecordEnv::shared_person`](super::RecordEnv::shared_person)).
316    pub fn shared_person<P: tilo_resource::SharedRecord>(&mut self) -> &mut Self {
317        let _ = self.storage.shared_person::<P>();
318        self
319    }
320
321    /// The settings section `T`, a resource model ([`RecordEnv::settings`](super::RecordEnv::settings)).
322    pub fn settings<T>(&mut self) -> &mut Self
323    where
324        T: Resource<State = T> + Fields + SettingsSchema,
325    {
326        let settings = self.storage.storage().settings::<T>();
327        let _ = self.storage.section(&settings);
328        self
329    }
330}
331
332impl<R: Resource, T: 'static> ViewCx<R, T> {
333    /// A part of the view over another record than the app's own — the
334    /// person's own record ([`RecordEnv::person`](super::RecordEnv::person)), a settings section
335    /// ([`RecordEnv::settings`](super::RecordEnv::settings)) — built like a view of that record: its
336    /// controls bind the record's paths
337    /// (`cx.checkbox("high-contrast", Appearance::paths().high_contrast)`)
338    /// and verbs, its edits go to that record as the person, recorded in its
339    /// history, and the part follows every change of it. `None` where the
340    /// record cannot be held (a view of a past version).
341    pub fn record<P: Resource>(
342        &self,
343        record: &impl OtherRecord<P>,
344        part: impl FnOnce(&mut ViewCx<P>) -> Element,
345    ) -> Option<Element> {
346        let held = record.held(sealed::Storage(&self.storage))?;
347        let dispatch = record_dispatch(&self.dispatch, &held);
348        let actions = record_actions(
349            self.dispatch.app_id(),
350            &self.storage,
351            self.alive.clone(),
352            Rc::clone(&held) as Rc<dyn RecordDoor>,
353        );
354        let mut cx = ViewCx::<P> {
355            env: super::env::RecordEnv {
356                live: held.live().clone(),
357                alive: self.alive.clone(),
358                dispatch,
359                saved: self.saved.clone(),
360                run: self.run.clone(),
361                storage: Rc::clone(&self.storage),
362                effects: Effects::inert(),
363            },
364            focus: Rc::new(|state| Some(state)),
365            updaters: Updaters::new(),
366            gate: None,
367            action_bindings: actions,
368            item: None,
369            path: Some(Vec::new()),
370            controls: Rc::clone(&self.controls),
371            forms: Forms::default(),
372            stages: self.stages.clone(),
373            feeds: HistoryFeeds::default(),
374            bindings: Bindings::new(),
375        };
376        let element = part(&mut cx);
377        let updaters = cx.updaters;
378        let followed = Cell::new(held.live().revision());
379        self.storage.follow_with(self.alive.clone(), move || {
380            held.sync();
381            let revision = held.live().revision();
382            if followed.replace(revision) != revision {
383                updaters.run(&held.live().get(), &Change::Everything);
384            }
385        });
386        Some(element)
387    }
388}
389
390/// The published actions of `held`, kept current with the record while
391/// `alive` lives: what a record's view part binds, and a surface's own
392/// controls and live rows ([`Actions::operation`]).
393fn record_actions<P: Resource>(
394    app: &'static str,
395    storage: &ViewStorage,
396    alive: std::rc::Weak<()>,
397    door: Rc<dyn RecordDoor>,
398) -> Actions<P> {
399    let actions = Actions::new(app, Rc::new(door.publish())).routed_to(door.id(), door.editable());
400    follow_publication(storage, alive, door, actions.table());
401    actions
402}
403
404/// Keep `table` current with `door`'s publication while `alive` lives.
405fn follow_publication(
406    storage: &ViewStorage,
407    alive: std::rc::Weak<()>,
408    door: Rc<dyn RecordDoor>,
409    table: super::controls::ActionTable,
410) {
411    let followed = Cell::new(door.revision());
412    storage.follow_with(alive, move || {
413        let revision = door.revision();
414        if followed.replace(revision) != revision {
415            table.update(door.publish());
416        }
417    });
418}
419
420impl<R: Resource> SurfaceCx<R> {
421    /// The published actions of another record than the app's own — the
422    /// person's own record, a settings section: a hand surface binds its
423    /// controls to the record's verbs and field edits with them, as to its
424    /// own, submits through [`Actions`]' bindings, and offers them on its
425    /// live rows ([`Actions::operation`]). `None` where the record cannot be
426    /// held.
427    pub fn record_actions<P: Resource>(&self, record: &impl OtherRecord<P>) -> Option<Actions<P>> {
428        let door = record.door(sealed::Storage(&self.storage))?.0;
429        Some(record_actions(
430            self.dispatch.app_id(),
431            &self.storage,
432            self.alive.clone(),
433            door,
434        ))
435    }
436}
437
438/// `parent`'s door, submitting to the held record `held`: what applies now
439/// applies to it as the person.
440fn record_dispatch<R: Resource, P: Resource>(
441    parent: &Dispatcher<R>,
442    held: &Rc<Held<P>>,
443) -> Dispatcher<P> {
444    let applying = Rc::downgrade(held);
445    let me = parent.me().map(str::to_owned);
446    parent.for_record(held.id(), !held.editable(), move |action| {
447        let held = applying
448            .upgrade()
449            .ok_or_else(|| ActionError::Refused("the record is closed".into()))?;
450        let me = me
451            .as_deref()
452            .ok_or_else(|| ActionError::Refused("nobody edits a past version".into()))?;
453        held.apply(action, &ActionInvoker::actor(me))
454    })
455}