Skip to main content

tilo_app/framework/
workspace.rs

1//! The records every app of the window's workspace opens beside its own
2//! ([`ui_core::record::workspace`]): the workspace's emoji among them,
3//! which any view of any app draws for `:name:`, and a conversation, which
4//! the two people it is between open from either side.
5//!
6//! Such a record is its owner's resource: the workspace's grants on the owner's
7//! node decide every change, here at the door as on the relay, whichever
8//! app's window makes it. The window opens it on first use, follows it
9//! change by change, and reports its route so the runtime syncs it; only
10//! the owner's own window publishes it (and lists it in its surface).
11
12use std::any::Any;
13use std::cell::RefCell;
14use std::collections::HashMap;
15use std::rc::Rc;
16
17use tilo_actions::{Action as _, ActionContext, ActionError, ActionInvoker, ChangeKind};
18use tilo_resource::{Invoke, Resource};
19use ui_core::LocalState;
20use ui_core::history_mode::HistoryOwner;
21use ui_core::permissions::{LivePermissions, PermissionScope, TreePath};
22use ui_core::record::StoredRecord;
23use ui_core::record::workspace::WorkspaceRecords;
24use ui_core::wake::WakeHandle;
25
26use super::live::Live;
27
28use super::chain::{Chain, Chained};
29
30/// A record every app of the window's workspace opens, held for the window
31/// ([`RecordEnv::workspace`](super::RecordEnv::workspace)).
32pub struct WorkspaceRecord<P: Resource> {
33    shared: Rc<Shared<P>>,
34}
35
36impl<P: Resource> Clone for WorkspaceRecord<P> {
37    fn clone(&self) -> Self {
38        Self {
39            shared: Rc::clone(&self.shared),
40        }
41    }
42}
43
44struct Shared<P: Resource> {
45    record: StoredRecord,
46    /// The app that keeps it: whose node the workspace's grants decide on.
47    owner: &'static str,
48    app_id: &'static str,
49    app_title: &'static str,
50    me: String,
51    permissions: Option<LivePermissions>,
52    /// The state, followed change by change.
53    live: Live<P>,
54    /// The same state as view state: a view that read it is built again
55    /// when another app's or device's change arrives.
56    state: LocalState<Rc<P::State>>,
57    /// Wakes the window: for an action that waits for the record.
58    wake: Option<WakeHandle>,
59    /// Actions asked for while the record could not be changed (its store
60    /// in use by whatever runs now), applied in order once it can.
61    waiting: RefCell<Vec<Waiting<P>>>,
62}
63
64/// An action that waits for its record ([`WorkspaceRecord::invoke_then`]),
65/// with what to do once it was applied or refused.
66struct Waiting<P: Resource> {
67    action: P::Action,
68    then: Box<dyn FnOnce(Result<(), ActionError>)>,
69    /// The window's refreshes it asked for: a few, so a record held
70    /// throughout them waits for the window's next refresh of its own
71    /// instead of keeping the window busy.
72    woken: u8,
73}
74
75/// How many refreshes an action that waits for its record asks for.
76const WAITING_WAKES: u8 = 4;
77
78/// What became of an action applied to the record.
79enum Applied {
80    Done(Result<(), ActionError>),
81    /// The record cannot be changed right now; nothing ran.
82    Waits,
83}
84
85impl<P: Resource> Shared<P> {
86    /// Follow the record when it moved. Whether it did.
87    fn refresh(&self) -> bool {
88        self.apply_waiting();
89        let moved = match self.record.read(|doc| self.live.sync(doc)) {
90            Some(Ok(moved)) => moved,
91            Some(Err(error)) => {
92                tracing::warn!(kind = P::KIND, %error, "workspace record unreadable");
93                false
94            }
95            None => false,
96        };
97        let _ = self.live.take_change();
98        if moved {
99            self.state.set(self.live.get());
100        }
101        moved
102    }
103
104    /// Apply the actions that wait for the record, in order, while it can
105    /// be changed: what still cannot waits on, asking the window for a
106    /// refresh a few times.
107    fn apply_waiting(&self) {
108        let Ok(mut waiting) = self.waiting.try_borrow_mut() else {
109            return;
110        };
111        if waiting.is_empty() {
112            return;
113        }
114        let queue = std::mem::take(&mut *waiting);
115        drop(waiting);
116        let mut left = Vec::new();
117        for entry in queue {
118            // Order is kept: once one waits, the ones after it wait too.
119            if !left.is_empty() {
120                left.push(entry);
121                continue;
122            }
123            match self.apply(&entry.action) {
124                Applied::Done(result) => (entry.then)(result),
125                Applied::Waits => left.push(entry),
126            }
127        }
128        let Some(first) = left.first_mut() else {
129            return;
130        };
131        let wake = first.woken < WAITING_WAKES;
132        if wake {
133            first.woken = first.woken.saturating_add(1);
134        }
135        let mut waiting = self.waiting.borrow_mut();
136        left.append(&mut waiting);
137        *waiting = left;
138        drop(waiting);
139        if wake && let Some(window) = &self.wake {
140            window.wake();
141        }
142    }
143
144    fn apply(&self, action: &P::Action) -> Applied {
145        self.apply_as(action, None)
146    }
147
148    /// [`Self::apply`] as `invoker` (an agent, a script), which the change is
149    /// attributed to and which [`ActionCx::author`](tilo_resource::action)
150    /// answers inside the action; the person seated here when `None`.
151    fn apply_as(&self, action: &P::Action, invoker: Option<&ActionInvoker>) -> Applied {
152        let encoded = match tilo_resource::encode::<P>(action) {
153            Ok(encoded) => encoded,
154            Err(error) => return Applied::Done(Err(error)),
155        };
156        let change = action.change_kind();
157        // Who the change is: the invoker's own actor when the host names one (an agent acting in
158        // a turn, attributed as it is at an app's record origin), else the person seated here.
159        let me = invoker
160            .and_then(|invoker| (!invoker.actor.is_empty()).then_some(invoker.actor.clone()));
161        let actor = me.as_deref().unwrap_or(&self.me);
162        let context = ActionContext::for_resource(P::KIND, P::KIND, P::LABEL)
163            .with_app(self.app_id, self.app_title)
164            .with_invoker(
165                invoker
166                    .cloned()
167                    .unwrap_or_else(|| ActionInvoker::actor(actor)),
168            )
169            .with_change(change);
170        let edited = self.record.try_edit(change == ChangeKind::Content, |doc| {
171            let writer = ui_core::history_source::authors::device_actor(actor, doc.get_actor());
172            tilo_actions::apply_local_with_context(doc, action, encoded, Some(&writer), context)
173                .map(|_| ())
174                .map_err(|error| error.to_string())
175        });
176        match edited {
177            Ok(result) => Applied::Done(result.map_err(ActionError::Refused)),
178            Err(unavailable) if unavailable.is_momentary() => Applied::Waits,
179            Err(unavailable) => Applied::Done(Err(ActionError::Refused(unavailable.to_string()))),
180        }
181    }
182}
183
184impl<P: Resource> WorkspaceRecord<P> {
185    /// The record `P` that `owner` keeps in the workspace `records` opens,
186    /// created with the resource's genesis where nothing holds it yet and
187    /// brought to its version: what an app window holds
188    /// ([`RecordEnv::workspace`](super::RecordEnv::workspace)), and what a
189    /// host that is no app window (the session's workspace settings) opens
190    /// the same way. Changes are attributed to `app` (its id and title) and,
191    /// where the host knows the workspace's grants as `permissions`, decided by
192    /// them first; `wake` is woken when another replica's change arrives.
193    pub fn open(
194        records: &WorkspaceRecords,
195        owner: &'static str,
196        app: (&'static str, &'static str),
197        permissions: Option<LivePermissions>,
198        wake: Option<WakeHandle>,
199    ) -> Option<Self> {
200        let opened = records.open_record(owner, P::KIND, &|doc| {
201            P::initialize(doc).map_err(|error| error.to_string())
202        });
203        Self::hold(
204            opened,
205            owner,
206            app,
207            permissions,
208            wake,
209            records.author().0.clone(),
210        )
211    }
212
213    /// [`Self::open`] of the record `id` of `kind` `P` that `owner` keeps:
214    /// a record of the owner's that the workspace names otherwise than
215    /// [`Self::open`]'s, such as a conversation's, which the two people it
216    /// is between name. The workspace's grants decide its changes like any
217    /// other.
218    pub fn open_at(
219        records: &WorkspaceRecords,
220        owner: &'static str,
221        id: &str,
222        app: (&'static str, &'static str),
223        permissions: Option<LivePermissions>,
224        wake: Option<WakeHandle>,
225    ) -> Option<Self> {
226        let opened = records.open_record_at(owner, id, P::KIND, &|doc| {
227            P::initialize(doc).map_err(|error| error.to_string())
228        });
229        Self::hold(
230            opened,
231            owner,
232            app,
233            permissions,
234            wake,
235            records.author().0.clone(),
236        )
237    }
238
239    /// [`Self::open_at`] of the record `owner` keeps as its own primary one of
240    /// `P::KIND` in the workspace: the record an app keeps as itself (Chat's
241    /// of its chat, `chat.workspace`), which `open_record` cannot open because
242    /// the package's primary id is a sentinel no [`RecordId::parse`] accepts.
243    /// An app's own window opens it this way; so does the session, reading the
244    /// conversations its agents answer in.
245    pub fn open_primary(
246        records: &WorkspaceRecords,
247        owner: &'static str,
248        app: (&'static str, &'static str),
249        permissions: Option<LivePermissions>,
250        wake: Option<WakeHandle>,
251    ) -> Option<Self> {
252        let opened = records.open_primary(owner, P::KIND, &|doc| {
253            P::initialize(doc).map_err(|error| error.to_string())
254        });
255        Self::hold(
256            opened,
257            owner,
258            app,
259            permissions,
260            wake,
261            records.author().0.clone(),
262        )
263    }
264
265    /// [`Self::open`]'s record, opened (or not) by the store it is kept in:
266    /// brought to the resource's version, read, and followed change by
267    /// change.
268    fn hold(
269        opened: Result<StoredRecord, ui_core::record::RecordOpenError>,
270        owner: &'static str,
271        app: (&'static str, &'static str),
272        permissions: Option<LivePermissions>,
273        wake: Option<WakeHandle>,
274        me: String,
275    ) -> Option<Self> {
276        let record = opened
277            .map_err(|error| tracing::warn!(kind = P::KIND, %error, "workspace record unavailable"))
278            .ok()?;
279        if record.read(tilo_resource::evolution::needs_migrate::<P>)?
280            && let Err(error) = record.edit(false, |doc| {
281                tilo_resource::evolution::migrate::<P>(doc).map_err(|error| error.to_string())
282            })
283        {
284            tracing::warn!(kind = P::KIND, %error, "workspace record not migrated");
285        }
286        let reader = record
287            .read(tilo_resource::Reader::<P>::read)?
288            .map_err(|error| tracing::warn!(kind = P::KIND, %error, "workspace record unreadable"))
289            .ok()?;
290        if let Some(wake) = &wake {
291            record.wake_on_arrival(wake.clone());
292        }
293        let live = Live::new(reader);
294        Some(Self {
295            shared: Rc::new(Shared::<P> {
296                record,
297                owner,
298                app_id: app.0,
299                app_title: app.1,
300                me,
301                permissions,
302                state: LocalState::new(live.get()),
303                live,
304                wake,
305                waiting: RefCell::default(),
306            }),
307        })
308    }
309
310    /// The record's id, which names it in the workspace.
311    #[must_use]
312    pub fn id(&self) -> &ui_core::record::RecordId {
313        &self.shared.record.checkout().id
314    }
315
316    /// The record's state as the window follows it, for the chain kept
317    /// beside it ([`RecordEnv::chain_beside`](super::RecordEnv::chain_beside)).
318    pub(crate) fn live(&self) -> Live<P> {
319        self.shared.live.clone()
320    }
321
322    /// The current state.
323    pub fn get(&self) -> Rc<P::State> {
324        self.shared.refresh();
325        self.shared.state.with(Rc::clone)
326    }
327
328    /// Increases whenever the state changes.
329    pub fn revision(&self) -> u64 {
330        self.shared.refresh();
331        self.shared.live.revision()
332    }
333
334    /// The person who acts here.
335    pub fn me(&self) -> &str {
336        &self.shared.me
337    }
338
339    /// Whether the workspace's grants let the person invoke the verb `verb` of
340    /// the record, as its owner publishes it. A window that knows nothing
341    /// of the workspace's grants (a test host) lets the record decide.
342    pub fn may(&self, verb: &str) -> bool {
343        let Some(permissions) = &self.shared.permissions else {
344            return true;
345        };
346        let Some(declared) = P::declarations().into_iter().find(|decl| decl.id == verb) else {
347            return false;
348        };
349        permissions.check(&PermissionScope::Action {
350            node: tilo_actions::action_node(
351                &TreePath::root(),
352                self.shared.owner,
353                P::KIND,
354                self.shared.record.checkout().id.as_str(),
355                declared.field,
356                verb,
357            ),
358            kind: declared.kind,
359        })
360    }
361
362    /// Apply the action `invoke` names: decided by the workspace's grants on the
363    /// owner's node, then one change of the record, attributed to the
364    /// person, in its history and one undo step of theirs.
365    ///
366    /// # Errors
367    /// The arguments do not build the action, the grants refuse it, the
368    /// record does, or the record cannot be changed right now (for that,
369    /// [`Self::invoke_then`] waits).
370    pub fn invoke(&self, invoke: &Invoke<P>) -> Result<(), ActionError> {
371        let action = self.admit(invoke)?;
372        match self.shared.apply(&action) {
373            Applied::Done(result) => {
374                self.shared.refresh();
375                result
376            }
377            Applied::Waits => Err(ActionError::Refused(
378                ui_core::record::HistoryUnavailable::StoreInUse.to_string(),
379            )),
380        }
381    }
382
383    /// [`Self::invoke`] as `invoker` (an agent, a script), which the change is attributed to —
384    /// the agent's own words in Chat are written as the agent, not as the person it acts for.
385    /// The grants are the ones this record was opened with, as for the person's own call.
386    ///
387    /// # Errors
388    /// The arguments do not build the action, the grants refuse it, or the record does.
389    pub fn invoke_as(
390        &self,
391        invoker: &ActionInvoker,
392        invoke: &Invoke<P>,
393    ) -> Result<(), ActionError> {
394        let action = self.admit(invoke)?;
395        match self.shared.apply_as(&action, Some(invoker)) {
396            Applied::Done(result) => {
397                self.shared.refresh();
398                result
399            }
400            Applied::Waits => Err(ActionError::Refused(
401                ui_core::record::HistoryUnavailable::StoreInUse.to_string(),
402            )),
403        }
404    }
405
406    /// [`Self::invoke`], and `then` with what became of it: at once, or,
407    /// where the record cannot be changed right now (its store in use by
408    /// whatever runs as this is asked), once it can, on a refresh of the
409    /// window. Actions that wait are applied in the order they were asked
410    /// for, after the ones that waited before them.
411    pub fn invoke_then(
412        &self,
413        invoke: &Invoke<P>,
414        then: impl FnOnce(Result<(), ActionError>) + 'static,
415    ) {
416        let action = match self.admit(invoke) {
417            Ok(action) => action,
418            Err(error) => return then(Err(error)),
419        };
420        let shared = &self.shared;
421        let first = shared
422            .waiting
423            .try_borrow()
424            .is_ok_and(|waiting| waiting.is_empty());
425        if first && let Applied::Done(result) = shared.apply(&action) {
426            shared.refresh();
427            return then(result);
428        }
429        shared.waiting.borrow_mut().push(Waiting {
430            action,
431            then: Box::new(then),
432            woken: 0,
433        });
434        if let Some(window) = &shared.wake {
435            window.wake();
436        }
437    }
438
439    /// The action `invoke` names, once the workspace's grants admit it.
440    fn admit(&self, invoke: &Invoke<P>) -> Result<P::Action, ActionError> {
441        let action = invoke
442            .build(tilo_resource::origin_now_ms())
443            .map_err(ActionError::Refused)?;
444        if let Some(verb) = verb_of::<P>(&action)
445            && !self.may(&verb)
446        {
447            return Err(ActionError::Refused(format!(
448                "you may not change the workspace's {}",
449                P::LABEL.to_lowercase()
450            )));
451        }
452        Ok(action)
453    }
454
455    /// The record underneath, for a test or host that carries it between
456    /// devices itself.
457    #[must_use]
458    pub fn record(&self) -> &StoredRecord {
459        &self.shared.record
460    }
461}
462
463/// The verb `action` invokes, as its record publishes it; `None` for a
464/// field edit.
465fn verb_of<P: Resource>(action: &P::Action) -> Option<String> {
466    let mut doc = ui_core::automerge::AutoCommit::new();
467    P::initialize(&mut doc).ok()?;
468    match action.describe(&doc)?.what {
469        tilo_actions::Did::Verb(call) => Some(call.action.action),
470        _ => None,
471    }
472}
473
474/// The records of the window's workspace, for what settles after the view
475/// that asked for them: a reply, or a message another device sent
476/// ([`RecordEnv::workspace_handle`](super::RecordEnv::workspace_handle)).
477#[derive(Clone)]
478pub struct WorkspaceHandle {
479    state: Rc<WorkspaceState>,
480}
481
482impl WorkspaceHandle {
483    /// The record `id` of `P` that `owner` keeps: opened, created where
484    /// nothing holds it yet, and followed change by change. `None` where it
485    /// cannot be opened.
486    pub fn record<P: Resource>(&self, owner: &'static str, id: &str) -> Option<WorkspaceRecord<P>> {
487        self.state.open_at::<P>(owner, id)
488    }
489
490    /// Whether the record `id` of `P` that `owner` keeps is held already,
491    /// without opening or creating it.
492    #[must_use]
493    pub fn holds_record<P: Resource>(&self, owner: &'static str, id: &str) -> bool {
494        self.state.holds_record(owner, id, P::KIND)
495    }
496
497    /// The chain of `P` kept beside one of the workspace's records
498    /// ([`Chained::chain_beside`]): the messages of one direct conversation,
499    /// in parts of the record the two people share, opened in the store that
500    /// keeps that record. `None` where it cannot be opened.
501    pub fn chain_beside<P: Chained>(
502        &self,
503        owner: &'static str,
504        base: &WorkspaceRecord<P::Base>,
505    ) -> Option<Chain<P>> {
506        self.state.chain_beside::<P>(owner, base)
507    }
508
509    /// What wakes the window: for an answer that arrives later.
510    #[must_use]
511    pub fn wake(&self) -> Option<&WakeHandle> {
512        self.state.wake()
513    }
514}
515
516/// A record open in a window: its owner and kind, and which record of them
517/// (the workspace's one of the kind, or a pair's).
518type OpenKey = (&'static str, &'static str, String);
519
520/// The records every app of the workspace opens that a window's views opened:
521/// one [`WorkspaceRecord`] per kind.
522pub(crate) struct WorkspaceState {
523    records: WorkspaceRecords,
524    app_id: &'static str,
525    app_title: &'static str,
526    permissions: Option<LivePermissions>,
527    /// Wakes the window when another replica's change arrives.
528    wake: Option<WakeHandle>,
529    opened: RefCell<HashMap<OpenKey, Rc<dyn Any>>>,
530    /// The chains kept beside the opened records (a chat's message parts,
531    /// [`WorkspaceHandle::chain_beside`]).
532    chains: super::chain::Chains,
533    owners: RefCell<Vec<HistoryOwner>>,
534    refreshers: RefCell<Vec<Box<dyn Fn() -> bool>>>,
535}
536
537impl WorkspaceState {
538    pub(crate) fn new(
539        records: WorkspaceRecords,
540        app_id: &'static str,
541        app_title: &'static str,
542        permissions: Option<LivePermissions>,
543        wake: Option<WakeHandle>,
544    ) -> Self {
545        Self {
546            records,
547            app_id,
548            app_title,
549            permissions,
550            wake,
551            opened: RefCell::default(),
552            chains: super::chain::Chains::default(),
553            owners: RefCell::default(),
554            refreshers: RefCell::default(),
555        }
556    }
557
558    /// What wakes the window: for an answer that arrives later, such as a
559    /// chooser's.
560    pub(crate) fn wake(&self) -> Option<&WakeHandle> {
561        self.wake.as_ref()
562    }
563
564    /// This state as the window's views hold it on
565    /// ([`RecordEnv::workspace_handle`](super::RecordEnv::workspace_handle)).
566    pub(crate) fn handle(self: &Rc<Self>) -> WorkspaceHandle {
567        WorkspaceHandle {
568            state: Rc::clone(self),
569        }
570    }
571
572    /// The window's app.
573    pub(crate) fn app_id(&self) -> &'static str {
574        self.app_id
575    }
576
577    /// Whether the record `id` of `kind` that `owner` keeps is held already,
578    /// without opening or creating it: what a list asks.
579    pub(crate) fn holds_record(&self, owner: &str, id: &str, kind: &str) -> bool {
580        self.records
581            .holds_record(owner, id, kind)
582            .is_ok_and(|held| held)
583    }
584
585    /// The record `P` that `owner` keeps in the workspace, opened on first use
586    /// and created with the resource's genesis where it does not exist yet.
587    pub(crate) fn open<P: Resource>(&self, owner: &'static str) -> Option<WorkspaceRecord<P>> {
588        let id = ui_core::record::workspace::record_id(P::KIND);
589        self.open_with::<P>(owner, &id, |records| {
590            WorkspaceRecord::open(
591                records,
592                owner,
593                (self.app_id, self.app_title),
594                self.permissions.clone(),
595                self.wake.clone(),
596            )
597        })
598    }
599
600    /// The record `id` of `P` that `owner` keeps, opened on first use and
601    /// created with the resource's genesis where it does not exist yet: a
602    /// conversation's record, named by the two people it is between.
603    pub(crate) fn open_at<P: Resource>(
604        &self,
605        owner: &'static str,
606        id: &str,
607    ) -> Option<WorkspaceRecord<P>> {
608        self.open_with::<P>(owner, id, |records| {
609            WorkspaceRecord::open_at(
610                records,
611                owner,
612                id,
613                (self.app_id, self.app_title),
614                self.permissions.clone(),
615                self.wake.clone(),
616            )
617        })
618    }
619
620    /// [`Self::open`]'s and [`Self::open_at`]'s record, opened once per
621    /// window: held for the views that ask again, and refreshed with them.
622    fn open_with<P: Resource>(
623        &self,
624        owner: &'static str,
625        id: &str,
626        open: impl FnOnce(&WorkspaceRecords) -> Option<WorkspaceRecord<P>>,
627    ) -> Option<WorkspaceRecord<P>> {
628        let key = (owner, P::KIND, id.to_owned());
629        if let Some(open) = self.opened.borrow().get(&key) {
630            return Rc::clone(open)
631                .downcast::<Shared<P>>()
632                .ok()
633                .map(|shared| WorkspaceRecord { shared });
634        }
635        // The owner's own window lists the record in its surface: its
636        // changes are decided against the owner's surface.
637        if owner == self.app_id {
638            super::controls::opened_record::<P>();
639        }
640        let opened = open(&self.records);
641        self.held(key, opened)
642    }
643
644    /// The chain of `P` kept beside one of the workspace's records
645    /// ([`Chained::chain_beside`]), opened on first use in the store that keeps
646    /// that record: a conversation's messages, decided by the workspace's
647    /// grants on the app like the record they are kept beside. `None` where it
648    /// cannot be opened.
649    pub(crate) fn chain_beside<P: Chained>(
650        &self,
651        owner: &'static str,
652        base: &WorkspaceRecord<P::Base>,
653    ) -> Option<Chain<P>> {
654        super::controls::opened_record::<P>();
655        let workspace = super::chain::WorkspaceRecords {
656            store: self.records.store_of(owner).ok()?,
657            me: self.records.author().0.clone(),
658            permissions: self.permissions.clone(),
659            app_id: self.app_id,
660            app_title: self.app_title,
661        };
662        let live = base.live();
663        let chain = P::chain_beside(base.id());
664        let state = self
665            .chains
666            .open_at::<P>(&workspace, &chain, move || live.get())?;
667        Some(Chain::applying(&state, self.app_id, base.me()))
668    }
669
670    /// Keep an opened record for the window: its route reported to the
671    /// runtime, its changes followed, and the record held for the views that
672    /// ask for it again.
673    fn held<P: Resource>(
674        &self,
675        key: OpenKey,
676        opened: Option<WorkspaceRecord<P>>,
677    ) -> Option<WorkspaceRecord<P>> {
678        let WorkspaceRecord { shared } = opened?;
679        self.owners.borrow_mut().push(shared.record.history_owner());
680        let weak = Rc::downgrade(&shared);
681        self.refreshers.borrow_mut().push(Box::new(move || {
682            weak.upgrade().is_some_and(|shared| shared.refresh())
683        }));
684        self.opened
685            .borrow_mut()
686            .insert(key, Rc::clone(&shared) as Rc<dyn Any>);
687        Some(WorkspaceRecord { shared })
688    }
689
690    /// Read every open record that moved. Whether one did.
691    pub(crate) fn refresh(&self) -> bool {
692        let moved = self
693            .refreshers
694            .borrow()
695            .iter()
696            .fold(false, |moved, refresh| refresh() | moved);
697        // A part another device wrote into a chain kept beside a record is
698        // read here, so the app's views show it.
699        for chain in self.chains.doors() {
700            let _ = chain.revision();
701        }
702        moved
703    }
704
705    /// The routes of the open records, which the runtime syncs.
706    pub(crate) fn owners(&self) -> Vec<HistoryOwner> {
707        self.owners.borrow().clone()
708    }
709}