Skip to main content

tilo_app/framework/
effect.rs

1//! Effects and jobs: work with no document delta, invoked through the same
2//! door as actions, answered by the app.
3//!
4//! An app names its effects as a typed vocabulary (`#[derive(Effect)]` on an
5//! enum, see [`tilo_resource::effect`]) and handles them where the state they
6//! need lives: a view registers a handler with
7//! [`ViewCx::effects`](super::ViewCx::effects), a custom surface implements
8//! [`ResourceSurface::effect`](super::ResourceSurface::effect). The framework
9//! publishes the vocabulary on the resource (with the availability and labels
10//! the handler reports right now), routes every invocation — a button, the
11//! palette, an agent, a script — to the handler as a typed value, and runs the
12//! job the handler starts: progress, cancellation and the result go back to
13//! whoever invoked it. A job's completion runs on the UI thread with the
14//! [`Dispatcher`], so what the effect changes in the document it changes with
15//! ordinary actions.
16//!
17//! An outside consequence a verb queued in an
18//! [`Outbox`](tilo_resource::Outbox) is carried out with
19//! [`RecordEnv::outbox`](super::RecordEnv::outbox): the framework runs the app's
20//! handler in the live app on the device that owns the entry and records how
21//! it settled.
22
23use std::any::Any;
24use std::cell::{Cell, RefCell};
25use std::collections::HashMap;
26use std::hash::Hash;
27use std::rc::{Rc, Weak};
28
29mod causal;
30mod job;
31mod window;
32use std::sync::Arc;
33use std::sync::atomic::{AtomicBool, Ordering};
34use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
35use ui_core::automerge::transaction::Transactable;
36
37use tilo_actions::{
38    ActionFailure, ActionOutput, ActionSpec, Availability, InvocationId, InvocationUpdate,
39};
40use tilo_resource::{ActionRef, Effect, Id, Outbox, Path, Resource, Settled};
41use ui_core::host_operation::{ResourceOperation, ResourceOperationStart};
42use ui_core::invocation_scope::{ResourceEffectScope, ResourceInvocationScope};
43use ui_core::portal::{
44    ContentsOutcome, DestinationOutcome, PickRequest, PickedContents, PortalOutcome,
45    SaveDestination, SaveOutcome,
46};
47use ui_core::{Arrival, WakeHandle};
48
49use super::dispatch::Dispatcher;
50use super::live::Live;
51
52fn attach_approved_check_reporter(
53    checked: ui_core::invocation_scope::ApprovedCheckReportedWork,
54    progress: Progress,
55) -> ui_core::invocation_scope::ApprovedCheckFuture {
56    checked(Box::new(move |observation| {
57        progress.with_effect(|| {
58            match observation {
59                ui_core::invocation_scope::ApprovedCheckReportObservation::Reported {
60                    phase,
61                    completed,
62                    total,
63                } => {
64                    progress.report(phase, completed, total);
65                }
66                ui_core::invocation_scope::ApprovedCheckReportObservation::Disabled { .. } => {
67                    progress.report(
68                        ui_core::i18n::I18nManager::lookup_plain("approved-check-reports-disabled"),
69                        None,
70                        None,
71                    );
72                }
73            }
74            Ok(())
75        })
76    }))
77}
78
79/// A file the person chose with the host's chooser
80/// ([`EffectCx::choose_file`]).
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ChosenFile {
83    /// The name shown to the person.
84    pub name: String,
85    /// The media type the host reported, when it did.
86    pub media_type: Option<String>,
87    pub bytes: Vec<u8>,
88    /// The file's durable identity on this device, when the host chose a
89    /// file it can name: the same file chosen again, even renamed or moved,
90    /// has the same identity, so an import can tell it already came in.
91    /// `None` where files have no paths (a browser page).
92    pub identity: Option<ui_core::record::RecordId>,
93}
94
95impl From<PickedContents> for ChosenFile {
96    fn from(picked: PickedContents) -> Self {
97        Self {
98            identity: picked
99                .location
100                .as_deref()
101                .map(ui_core::record::RecordId::identify_file),
102            name: picked.name,
103            media_type: picked.media_type,
104            bytes: picked.bytes,
105        }
106    }
107}
108
109/// Why a file chooser answered nothing.
110const CHOOSER_GONE: &str = "the file chooser closed without answering";
111
112/// What an effect handler (or a job's completion) answers.
113#[derive(Clone, Debug, PartialEq)]
114pub enum Effected {
115    /// Finished: the output goes back to whoever invoked the effect.
116    Done(ActionOutput),
117    /// Still running: the job started through the [`EffectCx`] answers later.
118    Pending,
119}
120
121impl Effected {
122    /// Finished, with nothing to report.
123    pub fn done() -> Self {
124        Self::Done(ActionOutput::default())
125    }
126
127    /// Finished, answering with `value` (an effect declared `returns`, or a
128    /// job's result).
129    pub fn value(value: impl serde::Serialize) -> Self {
130        Self::Done(ActionOutput {
131            value: Some(serde_json::to_value(value).unwrap_or(serde_json::Value::Null)),
132            resources: Vec::new(),
133        })
134    }
135
136    /// An outbox entry carried out, or failed for good: how it settled
137    /// ([`RecordEnv::outbox`](super::RecordEnv::outbox)). [`Self::value`] with the
138    /// result alone settles it done.
139    pub fn settled<O: serde::Serialize>(settled: &Settled<O>) -> Self {
140        Self::value(settled)
141    }
142
143    /// Finished, answering with the content of `hash` in this device's
144    /// content store (an effect declared `content`), which another app's
145    /// content-collecting operation takes.
146    pub fn content(hash: &str) -> Result<Self, String> {
147        let resource = tilo_actions::content_resource(hash)
148            .ok_or_else(|| format!("{hash:?} is not a content hash"))?;
149        Ok(Self::Done(ActionOutput {
150            value: None,
151            resources: vec![resource],
152        }))
153    }
154}
155
156/// What a handler or a job's completion returns: the answer, or why the
157/// effect failed.
158pub type EffectAnswer = Result<Effected, String>;
159
160/// A message from a running job to the UI thread.
161enum JobEvent {
162    Progress {
163        phase: String,
164        completed: Option<u64>,
165        total: Option<u64>,
166    },
167    Finished(Box<dyn Any + Send>),
168    Failed(ActionFailure),
169}
170
171/// A running job's channel back to the app: report progress, notice
172/// cancellation. Cloneable and `Send`, for the worker.
173pub struct Progress {
174    events: Sender<JobEvent>,
175    cancelled: Arc<AtomicBool>,
176    wake: WakeHandle,
177    custody: Option<ResourceEffectScope>,
178}
179
180// Written out rather than derived: a derived `clone` is inlined into every
181// app that clones a `Progress`, and this one is compiled once, in the
182// runtime.
183impl Clone for Progress {
184    fn clone(&self) -> Self {
185        Self {
186            events: self.events.clone(),
187            cancelled: Arc::clone(&self.cancelled),
188            wake: self.wake.clone(),
189            custody: self.custody.clone(),
190        }
191    }
192}
193
194impl Progress {
195    /// Report where the job is: a phase, and how far through it.
196    pub fn report(&self, phase: impl Into<String>, completed: Option<u64>, total: Option<u64>) {
197        if self
198            .events
199            .send(JobEvent::Progress {
200                phase: phase.into(),
201                completed,
202                total,
203            })
204            .is_ok()
205        {
206            self.wake.wake();
207        }
208    }
209
210    /// Whether the caller cancelled the job (an effect declared
211    /// `cancellable`). A worker checks between units of work and stops; its
212    /// result is discarded. For caller-scoped work this also performs a fresh,
213    /// potentially blocking authority check. Do not call while holding an owner
214    /// store guard that the authority check may need.
215    pub fn is_cancelled(&self) -> bool {
216        self.with_effect(|| Ok(())).is_err()
217    }
218
219    /// Share the actual job's cancellation flag with blocking native I/O.
220    /// This is explicit cancellation, not authority: callers still use
221    /// [`Self::with_effect`] at work boundaries to recheck current custody.
222    /// Treat the returned flag as read-only.
223    #[must_use]
224    pub fn cancellation_signal(&self) -> Arc<AtomicBool> {
225        Arc::clone(&self.cancelled)
226    }
227
228    /// Recheck the original caller immediately before a unit of external work.
229    /// Blocking workers should use this between units as well as checking
230    /// cancellation. It grants no access to the canonical document.
231    /// The authority check may block; release owner store guards before calling.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if the job was cancelled, the original caller's authority
236    /// lapsed, or the unit of work returned an error.
237    pub fn with_effect<T>(&self, effect: impl FnOnce() -> Result<T, String>) -> Result<T, String> {
238        if self.cancelled.load(Ordering::Acquire) {
239            return Err("the effect was cancelled".into());
240        }
241        let admitted = || {
242            // Authority checks may block while cancellation arrives. Recheck at
243            // the actual work boundary, not only before acquiring authority.
244            if self.cancelled.load(Ordering::Acquire) {
245                return Err("the effect was cancelled".into());
246            }
247            effect()
248        };
249        match &self.custody {
250            Some(custody) => custody.with_effect(admitted),
251            None => admitted(),
252        }
253    }
254
255    fn fail(&self, error: String) {
256        self.cancelled.store(true, Ordering::Release);
257        if self.events.send(JobEvent::Failed(failure(error))).is_ok() {
258            self.wake.wake();
259        }
260    }
261
262    fn finish(&self, value: Box<dyn Any + Send>) {
263        if self.events.send(JobEvent::Finished(value)).is_ok() {
264            self.wake.wake();
265        }
266    }
267}
268
269/// The answer of an effect, given later from wherever its work settles — a
270/// reply from another service, a callback, a worker. `Send`. Dropping it
271/// unanswered fails the effect.
272pub struct EffectReply {
273    progress: Progress,
274    completion: bool,
275}
276
277impl EffectReply {
278    /// Report progress, as [`Progress::report`].
279    pub fn progress(&self) -> &Progress {
280        &self.progress
281    }
282
283    /// Whether the caller cancelled the effect.
284    pub fn is_cancelled(&self) -> bool {
285        self.progress.is_cancelled()
286    }
287
288    /// Answer with `result`; a failure is told to the caller as its
289    /// message.
290    pub fn finish(self, result: Result<ActionOutput, String>) {
291        self.answer(result.map_err(failure));
292    }
293
294    /// Answer with `result`, a failure carrying its code (`portal_failed`,
295    /// `device_busy`) for programs and whether trying again may help.
296    pub fn answer(self, result: Result<ActionOutput, ActionFailure>) {
297        if self.completion {
298            match result {
299                Ok(output) => self.progress.finish(Box::new(output)),
300                Err(error) => {
301                    if self.progress.events.send(JobEvent::Failed(error)).is_ok() {
302                        self.progress.wake.wake();
303                    }
304                }
305            }
306        } else {
307            self.progress.finish(Box::new(result));
308        }
309    }
310}
311
312impl std::fmt::Debug for EffectReply {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        f.debug_struct("EffectReply").finish_non_exhaustive()
315    }
316}
317
318type Then<R, H> = Box<dyn FnOnce(&mut H, Box<dyn Any + Send>, &mut EffectCx<R, H>) -> EffectAnswer>;
319
320/// An answer a job waits for without a worker: polled on the UI thread each
321/// time the job is, `Some` once it arrived.
322type Source = Box<dyn FnMut() -> Option<Box<dyn Any + Send>>>;
323
324use job::{JobCore, Tracking, track_for};
325
326/// What [`JobCore::next`] found.
327enum Next {
328    Progress(Polled),
329    /// The job failed or lost its custody: it is over.
330    Failed(ActionFailure),
331    Finished(Box<dyn Any + Send>),
332}
333
334/// The answer a job without a completion of its own gave.
335fn answered(value: Box<dyn Any + Send>) -> Polled {
336    Polled::Finished(Some(
337        match value.downcast::<Result<ActionOutput, ActionFailure>>() {
338            Ok(result) => *result,
339            Err(_) => Err(failure("the effect answered with another type".into())),
340        },
341    ))
342}
343
344/// What a completion's answer, with the job it may have chained, comes to.
345fn completed(answer: EffectAnswer, chained: bool) -> Option<Result<ActionOutput, ActionFailure>> {
346    match answer {
347        Ok(Effected::Pending) if chained => None,
348        Ok(Effected::Pending) => Some(Err(failure(
349            "the effect's completion answered later but started no job".into(),
350        ))),
351        Ok(Effected::Done(output)) => Some(Ok(output)),
352        Err(error) => Some(Err(failure(error))),
353    }
354}
355
356/// A job a handler or a completion started: what it reports, and the
357/// completion that runs with what it finished with. The same type for
358/// every resource and host, so one copy of the job bookkeeping serves them
359/// all; only running the completion is typed ([`complete`]).
360struct Job {
361    core: JobCore,
362    /// The completion, a `Then<R, H>`.
363    then: Option<Box<dyn Any>>,
364    /// The `Then<R, H>` type the job was started for: whose jobs it is.
365    owner: std::any::TypeId,
366}
367
368impl Job {
369    fn stop(self) {
370        self.core.stop();
371    }
372}
373
374/// The custody a job runs under: its worker's, and the invocation's its
375/// completion is checked against.
376struct JobCustody {
377    worker: Option<ResourceEffectScope>,
378    custody: Option<ResourceInvocationScope>,
379}
380
381impl JobCustody {
382    /// The custody of a job an effect running under `worker` and `custody`
383    /// starts.
384    #[inline(never)]
385    fn of(worker: Option<&ResourceEffectScope>, custody: Option<&ResourceInvocationScope>) -> Self {
386        Self {
387            worker: worker.cloned(),
388            custody: custody.cloned(),
389        }
390    }
391}
392
393/// `custody` owned, for an effect context to keep.
394#[inline(never)]
395fn owned_scope(custody: Option<&ResourceInvocationScope>) -> Option<ResourceInvocationScope> {
396    custody.cloned()
397}
398
399/// Start tracking a job in `started` for `owner`, with the completion
400/// `then`; the [`Progress`] its work reports through. A job already
401/// started there is stopped: an effect runs one job at a time.
402fn track_job(
403    started: &mut Option<Job>,
404    wake: &WakeHandle,
405    custody: JobCustody,
406    then: Option<Box<dyn Any>>,
407    owner: std::any::TypeId,
408    cancel_by: Option<Arc<AtomicBool>>,
409) -> Progress {
410    let (events, receiver) = channel();
411    let signals = job::Signals::own();
412    let progress = Progress {
413        events,
414        cancelled: Arc::clone(&signals.cancelled),
415        wake: wake.clone(),
416        custody: custody.worker,
417    };
418    if let Some(previous) = started.replace(Job {
419        core: JobCore::of(
420            receiver,
421            job::Signals {
422                cancel_by,
423                ..signals
424            },
425            custody.custody,
426        ),
427        then,
428        owner,
429    }) {
430        tracing::warn!("an effect started a second job; the first one is stopped");
431        previous.stop();
432    }
433    progress
434}
435
436/// Run `run`, a handler or a completion, for `invocation` and under
437/// `custody` when it has one: it fails once the custody is gone.
438fn run_under(
439    invocation: Option<InvocationId>,
440    custody: Option<&ResourceInvocationScope>,
441    run: &mut dyn FnMut() -> EffectAnswer,
442) -> EffectAnswer {
443    let _acting = ActingFor::enter(invocation);
444    match custody {
445        Some(custody) => custody
446            .with_effect(run)
447            .and_then(|answer| custody.check().map(|()| answer)),
448        None => run(),
449    }
450}
451
452/// A job's completion run: the completion, what the job finished with, its
453/// custody and the invocation it answers; what it answered and the job it
454/// chained, if any.
455type Complete<'a> = dyn FnMut(
456        Box<dyn Any>,
457        Box<dyn Any + Send>,
458        Option<ResourceInvocationScope>,
459        Option<InvocationId>,
460    ) -> Result<(EffectAnswer, Option<Job>), String>
461    + 'a;
462
463/// Run a job's completion `then` (a `Then<R, H>`) with `host` and what the
464/// job finished with, on the UI thread: the typed part of polling a job.
465fn complete<R: Resource, H: 'static>(
466    env: &EffectEnv<R>,
467    host: &mut H,
468    then: Box<dyn Any>,
469    value: Box<dyn Any + Send>,
470    custody: Option<ResourceInvocationScope>,
471    invocation: Option<InvocationId>,
472) -> Result<(EffectAnswer, Option<Job>), String> {
473    let Ok(then) = then.downcast::<Then<R, H>>() else {
474        return Err("a job's completion is of another type".into());
475    };
476    let scope = owned_scope(custody.as_ref());
477    let mut cx = EffectCx::<R, H>::invoked(env.clone(), custody_parts(invocation, custody)?);
478    let mut then = Some(*then);
479    let mut value = Some(value);
480    let answer = run_under(
481        invocation,
482        scope.as_ref(),
483        &mut || match (then.take(), value.take()) {
484            (Some(then), Some(value)) => then(host, value, &mut cx),
485            _ => Err("a job's completion ran twice".into()),
486        },
487    );
488    Ok((answer, cx.inv.started.take()))
489}
490
491/// What every effect context of one resource shares.
492pub(crate) struct EffectEnv<R: Resource> {
493    pub(crate) dispatch: Dispatcher<R>,
494    pub(crate) live: Live<R>,
495    pub(crate) wake: WakeHandle,
496    scope: Option<std::rc::Weak<super::surface::SurfaceScope>>,
497}
498
499impl<R: Resource> Clone for EffectEnv<R> {
500    fn clone(&self) -> Self {
501        Self {
502            dispatch: self.dispatch.clone(),
503            live: self.live.clone(),
504            wake: self.wake.clone(),
505            scope: self.scope.clone(),
506        }
507    }
508}
509
510impl<R: Resource> EffectEnv<R> {
511    pub(crate) fn scoped(&self, scope: &Rc<super::surface::SurfaceScope>) -> Self {
512        let mut env = self.clone();
513        env.scope = Some(Rc::downgrade(scope));
514        env
515    }
516    fn active(&self) -> bool {
517        self.scope
518            .as_ref()
519            .is_none_or(|scope| scope.upgrade().is_some_and(|scope| scope.active()))
520    }
521}
522
523/// Tie `progress` to the surface `scope` when there is one: a job of a
524/// retired surface is cancelled.
525#[inline(never)]
526fn track_in(scope: Option<&std::rc::Weak<super::surface::SurfaceScope>>, progress: &Progress) {
527    if let Some(scope) = scope {
528        if let Some(scope) = scope.upgrade() {
529            scope.track(&progress.cancelled);
530        } else {
531            progress.cancelled.store(true, Ordering::Release);
532        }
533    }
534}
535
536/// What an effect handler, a job's completion and a projection drain
537/// receive: the resource, its dispatcher, and the means to start a job.
538///
539/// `H` is what a completion gets mutable access to: the surface that
540/// handles the effect (`&mut self`), or `()` for a handler a view registered.
541pub struct EffectCx<R: Resource, H = ()> {
542    env: EffectEnv<R>,
543    /// Everything else, which does not name the resource type.
544    inv: Answering,
545    /// What its completions get `&mut` access to.
546    host: std::marker::PhantomData<fn(&mut H)>,
547}
548
549/// What an effect context holds besides its resource: the invocation it
550/// answers and what came with it. Not generic, so making, moving and
551/// dropping it is compiled once rather than in every app's contexts.
552struct Answering {
553    invocation: Option<InvocationId>,
554    started: Option<Job>,
555    /// The resource's document while a surface's handler answers an
556    /// invocation, lent to it for reading.
557    document: Option<Rc<RefCell<tilo_resource::Doc>>>,
558    /// The lent document's heads, taken when it was lent.
559    heads: Vec<ui_core::automerge::ChangeHash>,
560    custody: Option<ResourceInvocationScope>,
561    worker_custody: Option<ResourceEffectScope>,
562    /// The invocation the host delivered for someone that this answers:
563    /// what another resource's action this effect calls continues
564    /// ([`EffectCx::call`]). `None` for the person at this device acting
565    /// here, and for a view's own job.
566    delivered: Option<InvocationId>,
567    target: Option<ui_core::host_operation::OperationTarget>,
568    /// Where the invoker asked the effect to act (`#[param(place)]`).
569    place: Option<tilo_resource::Place>,
570    /// The caller's cancellation signal, which a job this effect leaves to
571    /// answer later is stopped by when the caller gives up waiting
572    /// ([`EffectCx::cancel_by`]).
573    cancel_by: Option<Arc<AtomicBool>>,
574}
575
576impl Answering {
577    /// Answering `invocation`, with nothing else yet.
578    #[inline(never)]
579    fn of(invocation: Option<InvocationId>) -> Self {
580        Self {
581            invocation,
582            started: None,
583            document: None,
584            heads: Vec::new(),
585            custody: None,
586            worker_custody: None,
587            delivered: None,
588            target: None,
589            place: None,
590            cancel_by: None,
591        }
592    }
593}
594
595/// The place `effect` was told to act at: its argument declared
596/// `#[param(place)]`, when given.
597fn place_of<E: Effect>(
598    decl: &tilo_resource::effect::EffectDecl,
599    effect: &E,
600) -> Option<tilo_resource::Place> {
601    let param = place_param(decl)?;
602    place_from(effect.arguments().remove(param)?)
603}
604
605/// The name of the parameter `decl` declares `#[param(place)]`, if any.
606#[inline(never)]
607fn place_param(decl: &tilo_resource::effect::EffectDecl) -> Option<&str> {
608    decl.params
609        .iter()
610        .find(|param| param.is_place())
611        .map(|param| param.name.as_ref())
612}
613
614/// A place argument as given, read once in the runtime rather than in
615/// every app.
616#[inline(never)]
617fn place_from(value: serde_json::Value) -> Option<tilo_resource::Place> {
618    // ignored-failure: an argument that is not a place names no place, and the effect runs without one
619    serde_json::from_value(value).ok()
620}
621
622/// The resource as it was at a past version a view asked for: its
623/// document there, the heads that name it and the name a caller gives it
624/// back ([`EffectCx::version`]).
625pub struct DocumentVersion {
626    pub doc: tilo_resource::Doc,
627    pub heads: Vec<ui_core::automerge::ChangeHash>,
628    pub name: String,
629}
630
631/// An effect's future, its output boxed.
632type BoxedFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Box<dyn Any + Send>> + Send>>;
633
634/// An effect's blocking work, its output boxed.
635type BoxedWork = Box<dyn FnOnce(Progress) -> Box<dyn Any + Send> + Send>;
636
637/// Run an effect's future on the app runtime, polled under its custody, and
638/// deliver what it finished with. Every effect's future runs through this
639/// one function and task type.
640fn spawn_future(progress: Progress, future: BoxedFuture) -> ui_core::rt::JoinHandle<()> {
641    ui_core::rt::app().spawn(async move {
642        let mut future = future;
643        let value = std::future::poll_fn(|cx| {
644            match progress.with_effect(|| Ok(future.as_mut().poll(cx))) {
645                Ok(std::task::Poll::Ready(value)) => std::task::Poll::Ready(Ok(value)),
646                Ok(std::task::Poll::Pending) => std::task::Poll::Pending,
647                Err(error) => std::task::Poll::Ready(Err(error)),
648            }
649        })
650        .await;
651        match value {
652            Ok(value) => progress.finish(value),
653            Err(error) => progress.fail(error),
654        }
655    })
656}
657
658/// Run an effect's blocking work on the app runtime's blocking threads,
659/// under its custody, and deliver what it finished with ([`spawn_future`]).
660fn spawn_work(progress: Progress, work: BoxedWork) -> ui_core::rt::JoinHandle<()> {
661    ui_core::rt::app().spawn_blocking(move || {
662        match progress.with_effect(|| Ok(work(progress.clone()))) {
663            Ok(value) => progress.finish(value),
664            Err(error) => progress.fail(error),
665        }
666    })
667}
668
669impl<R: Resource, H: 'static> EffectCx<R, H> {
670    pub(crate) fn new(env: EffectEnv<R>, invocation: Option<InvocationId>) -> Self {
671        Self {
672            env,
673            inv: Answering::of(invocation),
674            host: std::marker::PhantomData,
675        }
676    }
677
678    /// The context of the invocation `parts` describe.
679    fn invoked(mut env: EffectEnv<R>, parts: InvocationParts) -> Self {
680        let InvocationParts { inv, lent } = parts;
681        if let Some(scope) = lent {
682            env.dispatch = env.dispatch.for_custody(scope);
683        }
684        Self {
685            env,
686            inv,
687            host: std::marker::PhantomData,
688        }
689    }
690
691    /// Where the invoker asked this effect to act (its `#[param(place)]`
692    /// argument): act there instead of at their caret. Once it succeeds
693    /// for a participant, they land there ([`Live::place_of`]).
694    pub fn place(&self) -> Option<&tilo_resource::Place> {
695        self.inv.place.as_ref()
696    }
697
698    /// The resource's document as it is now, to read what a view answers
699    /// with: the state at its heads, a selection's measurements, a picture
700    /// of it. Only a surface's handler answering an invocation has it; a
701    /// job's completion, which runs later, reads the live state instead.
702    /// Changes go through the dispatcher, never here.
703    pub fn document(&self) -> Option<std::cell::Ref<'_, tilo_resource::Doc>> {
704        self.inv.document.as_ref().map(|document| document.borrow())
705    }
706
707    /// The heads of the document as it is now; empty without one.
708    pub fn heads(&self) -> Vec<ui_core::automerge::ChangeHash> {
709        self.inv.heads.clone()
710    }
711
712    /// The resource at `version`, as a caller names one: a change id from
713    /// its history (or enough of one) or a checkpoint's name.
714    pub fn version(&self, version: &str) -> Result<DocumentVersion, String> {
715        let document = self
716            .inv
717            .document
718            .as_ref()
719            .ok_or_else(|| "this effect has no document to read".to_owned())?;
720        let mut doc = document
721            .try_borrow_mut()
722            .map_err(|_| "read a past version before borrowing the document".to_owned())?;
723        let heads = ui_core::history_source::steps::resolve(&doc, version)?;
724        let past = doc
725            .fork_at(&heads)
726            .map_err(|error| format!("version {version:?} is not in this document: {error}"))?;
727        Ok(DocumentVersion {
728            doc: past,
729            name: super::history::version_name(&heads),
730            heads,
731        })
732    }
733
734    /// Where the effect's document changes go: ordinary actions, checked,
735    /// applied, undone and replicated like any other.
736    pub fn dispatcher(&self) -> Dispatcher<R> {
737        self.env.dispatch.clone()
738    }
739
740    /// The person this invocation runs for: its invoker, when a host
741    /// delivered it for someone, else the participant the view is for.
742    /// `None` for a bot, an agent or an app acting, and in a past version.
743    /// An effect that answers or changes what is someone's own ("my link")
744    /// reads whose it is here, never from the view.
745    pub fn person(&self) -> Option<String> {
746        match ui_core::invocation_scope::current_invoker() {
747            Some(invoker) => {
748                (invoker.kind == tilo_actions::ActorKind::Person).then_some(invoker.actor)
749            }
750            None => self.env.dispatch.me().map(str::to_owned),
751        }
752    }
753
754    /// Submit one typed action, as [`Dispatcher::apply`].
755    pub fn apply(&self, action: &R::Action) {
756        self.env.dispatch.apply(action);
757    }
758
759    /// Submit a field edit or a verb, as [`Dispatcher::submit`].
760    pub fn submit(&self, submit: impl super::Submit<R>) {
761        self.env.dispatch.submit(submit);
762    }
763
764    /// The resource's live state.
765    pub fn live(&self) -> &Live<R> {
766        &self.env.live
767    }
768
769    /// The actual source invocation this work continues. Ordinary background
770    /// projection drains have none; causal drains retain the current Document
771    /// caller. Its source identity may differ from the local reply operation.
772    pub fn invocation(&self) -> Option<InvocationId> {
773        self.inv.invocation
774    }
775
776    /// Submit the exact approved moderation operation through the record's host.
777    /// Pass the returned Send future to the action's existing job.
778    pub fn external_moderation(
779        &self,
780        operation: registry_types::ExternalModerationOperation,
781    ) -> Result<ui_core::invocation_scope::ExternalModerationFuture, String> {
782        if let Some(custody) = &self.inv.custody {
783            return custody.external_moderation(operation);
784        }
785        let target = self
786            .inv
787            .target
788            .as_ref()
789            .ok_or("external moderation requires an exact dispatched record target")?;
790        if target.route().app_id != self.env.dispatch.app_id() {
791            return Err("external moderation dispatch app changed".into());
792        }
793        target.external_moderation(
794            self.inv
795                .invocation
796                .ok_or("external moderation requires a dispatched invocation")?,
797            operation,
798        )
799    }
800
801    /// Name the paths this invocation is about to write (absolute paths; a
802    /// folder covers what is under it), before writing them: when an agent
803    /// acting for the session's person invoked it, the versions those
804    /// writes make record that agent, for its person. The host takes who
805    /// acted from its own custody of the invocation, never from here. Wait
806    /// on the receipt where the writes run (a [`Self::thread`]), then write
807    /// whatever it answered. `None` when there is nobody to name: no
808    /// invocation's custody, or one that is not an agent's.
809    pub fn attribute_writes(
810        &self,
811        paths: Vec<String>,
812    ) -> Option<ui_core::invocation_scope::WriteAttributionReceipt> {
813        match self.inv.custody.as_ref()?.attribute_writes(paths) {
814            Ok(receipt) => receipt,
815            Err(error) => {
816                // The write stays its person's in file history, which reads
817                // as if they typed it: say so where the app's log will keep it.
818                tracing::warn!(%error, "these writes stay their person's in file history");
819                None
820            }
821        }
822    }
823
824    /// Run an approved project check through this effect's admitted host channel.
825    /// Commands, source paths and resource bounds are chosen by the project host.
826    /// Pass the returned future to the existing effect job with [`Self::spawn`].
827    pub fn approved_check(
828        &self,
829        check_id: &str,
830    ) -> Result<ui_core::invocation_scope::ApprovedCheckFuture, String> {
831        if let Some(custody) = &self.inv.custody {
832            return custody.approved_check(check_id);
833        }
834        let target = self
835            .inv
836            .target
837            .as_ref()
838            .ok_or("approved checks require an exact dispatched record target")?;
839        if target.route().app_id != self.env.dispatch.app_id() {
840            return Err("approved check dispatch app changed".into());
841        }
842        target.approved_check(
843            self.inv
844                .invocation
845                .ok_or("approved checks require a dispatched invocation")?,
846            check_id,
847        )
848    }
849
850    /// Attach approved-check reports to this action's existing job progress.
851    /// The app remains the sole publisher of invocation-update sequences.
852    pub fn approved_check_reported(
853        &self,
854        check_id: &str,
855    ) -> Result<
856        impl FnOnce(Progress) -> ui_core::invocation_scope::ApprovedCheckFuture
857        + Send
858        + 'static
859        + use<R, H>,
860        String,
861    > {
862        let checked = if let Some(custody) = &self.inv.custody {
863            custody.approved_check_reported(check_id)?
864        } else {
865            let target = self
866                .inv
867                .target
868                .as_ref()
869                .ok_or("approved checks require an exact dispatched record target")?;
870            if target.route().app_id != self.env.dispatch.app_id() {
871                return Err("approved check dispatch app changed".into());
872            }
873            target.approved_check_reported(
874                self.inv
875                    .invocation
876                    .ok_or("approved checks require a dispatched invocation")?,
877                check_id,
878            )?
879        };
880        Ok(move |progress: Progress| attach_approved_check_reporter(checked, progress))
881    }
882
883    /// Call another resource's published action, as part of this effect,
884    /// then `then` with what it answered (or why it did not go ahead) on the
885    /// UI thread.
886    ///
887    /// The host decides the call as this effect's caller: when it delivered
888    /// this invocation for someone (a person at its palette, an agent,
889    /// another member), the call continues it and is theirs, with their
890    /// grants, in the other resource's history and their undo there. The
891    /// person at this device acting here asks as this app, which needs the
892    /// workspace's grant to it (or their answer). What the effect then
893    /// submits to its own resource continues it the same way, so the whole
894    /// composition is the caller's, each part recorded once where it
895    /// changed something.
896    ///
897    /// ```ignore
898    /// let note = Call::on("so.tilo.notes", None, &notebook, "create-page")
899    ///     .with(json!({ "title": title }));
900    /// Ok(cx.call(&note, move |_, created, cx| {
901    ///     let created = created?;
902    ///     cx.submit(Tracker::LINK.with(..));
903    ///     cx.when_landed(|_, _, _| Ok(Effected::done()))
904    /// }))
905    /// ```
906    pub fn call(
907        &mut self,
908        call: &super::Call,
909        then: impl FnOnce(&mut H, Result<ActionOutput, String>, &mut Self) -> EffectAnswer + 'static,
910    ) -> Effected {
911        let action = call.action().to_owned();
912        let answer = self.env.dispatch.call_published(self.inv.delivered, call);
913        let mut answer = Some(answer);
914        self.wait_on(
915            move || match answer.take() {
916                Some(Ok(called)) => {
917                    if let Some(outcome) = called_outcome(&called, &action) {
918                        return Ok(outcome);
919                    }
920                    answer = Some(Ok(called));
921                    Err(TryRecvError::Empty)
922                }
923                Some(Err(refused)) => Ok(Err(refused)),
924                None => Err(TryRecvError::Disconnected),
925            },
926            move |host, outcome, cx| {
927                then(
928                    host,
929                    outcome.unwrap_or_else(|| Err("the call was dropped unanswered".into())),
930                    cx,
931                )
932            },
933        )
934    }
935
936    /// Wakes the app from another thread, to draw what a worker changed.
937    pub fn waker(&self) -> WakeHandle {
938        self.env.wake.clone()
939    }
940
941    fn track(&mut self, then: Option<Box<dyn Any>>) -> Progress {
942        track_for(
943            &mut Tracking {
944                started: &mut self.inv.started,
945                wake: &self.env.wake,
946                worker: self.inv.worker_custody.as_ref(),
947                custody: self.inv.custody.as_ref(),
948                scope: self.env.scope.as_ref(),
949                cancel_by: self.inv.cancel_by.as_ref(),
950                owner: std::any::TypeId::of::<Then<R, H>>(),
951            },
952            then,
953        )
954    }
955
956    /// `then` as a job's completion, which continues the invocation this
957    /// effect continues for whoever the host delivered it for.
958    fn then<T: Send + 'static>(
959        &self,
960        then: impl FnOnce(&mut H, T, &mut Self) -> EffectAnswer + 'static,
961    ) -> Box<dyn Any> {
962        let delivered = self.inv.delivered;
963        let then: Then<R, H> = Box::new(move |host, value, cx| match value.downcast::<T>() {
964            Ok(value) => {
965                cx.inv.delivered = cx.inv.delivered.or(delivered);
966                then(host, *value, cx)
967            }
968            Err(_) => Err("a job finished with a value of another type".into()),
969        });
970        Box::new(then)
971    }
972
973    /// Run `work`'s future on the app runtime, then `then` with its output on
974    /// the UI thread. The future reports through the [`Progress`] it is
975    /// built with; cancelling the effect aborts it.
976    pub fn spawn<T, F>(
977        &mut self,
978        work: impl FnOnce(Progress) -> F,
979        then: impl FnOnce(&mut H, T, &mut Self) -> EffectAnswer + 'static,
980    ) -> Effected
981    where
982        T: Send + 'static,
983        F: std::future::Future<Output = T> + Send + 'static,
984    {
985        let progress = self.track(Some(self.then(then)));
986        let future = work(progress.clone());
987        let task = spawn_future(
988            progress,
989            Box::pin(async move { Box::new(future.await) as Box<dyn Any + Send> }),
990        );
991        if let Some(started) = self.inv.started.as_mut() {
992            started.core.task = Some(task);
993        }
994        Effected::Pending
995    }
996
997    /// Run blocking `work` off the UI thread, then `then` with its result on
998    /// the UI thread. `work` reports through its [`Progress`] and checks it
999    /// for cancellation.
1000    pub fn thread<T>(
1001        &mut self,
1002        work: impl FnOnce(Progress) -> T + Send + 'static,
1003        then: impl FnOnce(&mut H, T, &mut Self) -> EffectAnswer + 'static,
1004    ) -> Effected
1005    where
1006        T: Send + 'static,
1007    {
1008        let progress = self.track(Some(self.then(then)));
1009        let task = spawn_work(
1010            progress,
1011            Box::new(move |progress| Box::new(work(progress)) as Box<dyn Any + Send>),
1012        );
1013        if let Some(started) = self.inv.started.as_mut() {
1014            started.core.task = Some(task);
1015        }
1016        Effected::Pending
1017    }
1018
1019    /// Have the job this effect leaves to answer later stopped when `signal`
1020    /// goes off, as well as when the caller cancels it.
1021    ///
1022    /// An effect whose answer is read from a long-lived surface ([`Self::reply`])
1023    /// can lose its caller before the caller's own cancellation reaches it: the
1024    /// answer is kept by the surface rather than by a job of this invocation.
1025    /// A handler of such an effect passes the caller's cancellation signal
1026    /// ([`Progress::cancellation_signal`]) here, and the job that carries the
1027    /// answer stops with it: an operation nobody waits for must not run on.
1028    pub fn cancel_by(&mut self, signal: Arc<AtomicBool>) {
1029        self.inv.cancel_by = Some(signal);
1030    }
1031
1032    /// Answer later, from wherever the work settles. Return
1033    /// [`Effected::Pending`] and keep the reply until then.
1034    pub fn reply(&mut self) -> EffectReply {
1035        let progress = self.track(None);
1036        EffectReply {
1037            progress,
1038            completion: false,
1039        }
1040    }
1041
1042    /// Answer later, then settle the successful receipt on the UI thread
1043    /// under this effect's retained invocation. Refusal skips the completion
1044    /// and preserves its structured failure. Cancellation and current custody
1045    /// are checked by the same job path as [`Self::spawn`].
1046    pub fn reply_then(
1047        &mut self,
1048        then: impl FnOnce(&mut H, ActionOutput, &mut Self) -> EffectAnswer + 'static,
1049    ) -> EffectReply {
1050        let progress = self.track(Some(self.then(then)));
1051        EffectReply {
1052            progress,
1053            completion: true,
1054        }
1055    }
1056
1057    /// Answer once what this effect submitted through its
1058    /// [`dispatcher`](Self::dispatcher) landed: each submission goes through
1059    /// the door like any other, checked again for the effect's invoker, and
1060    /// `then` runs with the last one's [`Landed`](super::Landed) — what the door answered
1061    /// (what an edit changed, as the resource's surface reports it) and the
1062    /// items it created — to reply to the effect's caller. A refused
1063    /// submission fails the effect with why. The effect must be declared
1064    /// `job`, since it answers after the edits apply.
1065    ///
1066    /// ```ignore
1067    /// cx.dispatcher().submit(Doc::BODY.edit(op));
1068    /// cx.when_landed(|_, landed, _| Ok(Effected::value(landed.answer)))
1069    /// ```
1070    ///
1071    /// # Errors
1072    /// When the effect was not invoked (a view's own job) or submitted
1073    /// nothing to wait for.
1074    pub fn when_landed(
1075        &mut self,
1076        then: impl FnOnce(&mut H, super::Landed, &mut Self) -> EffectAnswer + 'static,
1077    ) -> EffectAnswer {
1078        let invocation = self
1079            .inv
1080            .invocation
1081            .ok_or("only an invoked effect waits for what it submitted")?;
1082        let receiver = self
1083            .env
1084            .dispatch
1085            .wait_landed(invocation)
1086            .ok_or("the effect submitted nothing to wait for")?;
1087        Ok(self.wait_for(receiver, |host, landed, cx| match landed {
1088            Some(Ok(landed)) => then(host, landed, cx),
1089            Some(Err(refused)) => Err(refused),
1090            None => Err("the submission was dropped before it landed".into()),
1091        }))
1092    }
1093
1094    /// Run `then` on the UI thread with what `receiver` delivers, `None`
1095    /// when its sender went away unanswered. Nothing waits on a thread (a
1096    /// browser page has none to spare): the job looks each time the app
1097    /// wakes, so the sender must wake it, as the host's services do.
1098    fn wait_for<T: Send + 'static>(
1099        &mut self,
1100        receiver: Receiver<T>,
1101        then: impl FnOnce(&mut H, Option<T>, &mut Self) -> EffectAnswer + 'static,
1102    ) -> Effected {
1103        self.wait_on(move || receiver.try_recv(), then)
1104    }
1105
1106    /// [`Self::wait_for`] for an answer `poll` looks for, in the way of
1107    /// [`Receiver::try_recv`].
1108    fn wait_on<T: Send + 'static>(
1109        &mut self,
1110        mut poll: impl FnMut() -> Result<T, TryRecvError> + 'static,
1111        then: impl FnOnce(&mut H, Option<T>, &mut Self) -> EffectAnswer + 'static,
1112    ) -> Effected {
1113        let progress = self.track(Some(self.then(then)));
1114        if let Some(started) = self.inv.started.as_mut() {
1115            started.core.source = Some(Box::new(move || {
1116                // The job's own channel stays open while it waits here.
1117                let _waiting = &progress;
1118                match poll() {
1119                    Ok(value) => Some(Box::new(Some(value)) as Box<dyn Any + Send>),
1120                    Err(TryRecvError::Empty) => None,
1121                    Err(TryRecvError::Disconnected) => Some(Box::new(None::<T>)),
1122                }
1123            }));
1124        }
1125        Effected::Pending
1126    }
1127
1128    /// Ask the person to choose a file with the host's chooser (`request`
1129    /// is an Open request), then run `then` on the UI thread with its name
1130    /// and contents, or `None` when they cancelled. Works where files have
1131    /// no paths, such as a browser page. Bound reads with
1132    /// [`PickRequest::max_contents_bytes`]; an oversized selection fails the
1133    /// job before calling `then`. The effect must be declared `job`.
1134    pub fn choose_file(
1135        &mut self,
1136        request: PickRequest,
1137        then: impl FnOnce(&mut H, Option<ChosenFile>, &mut Self) -> EffectAnswer + 'static,
1138    ) -> Effected {
1139        let chosen = request.send_contents();
1140        self.wait_for(chosen, move |host, outcome, cx| match outcome {
1141            Some(ContentsOutcome::Picked(files)) => {
1142                then(host, files.into_iter().next().map(ChosenFile::from), cx)
1143            }
1144            Some(ContentsOutcome::Cancelled) => then(host, None, cx),
1145            Some(ContentsOutcome::Failed(reason)) => Err(reason),
1146            None => Err(CHOOSER_GONE.into()),
1147        })
1148    }
1149
1150    /// [`Self::choose_file`] for a request that lets the person choose
1151    /// several files ([`PickRequest::open_multiple`]): `then` receives every
1152    /// chosen file, none when they cancelled. A request contents limit applies
1153    /// to the total bytes across the entire selection.
1154    pub fn choose_files(
1155        &mut self,
1156        request: PickRequest,
1157        then: impl FnOnce(&mut H, Vec<ChosenFile>, &mut Self) -> EffectAnswer + 'static,
1158    ) -> Effected {
1159        let chosen = request.send_contents();
1160        self.wait_for(chosen, move |host, outcome, cx| match outcome {
1161            Some(ContentsOutcome::Picked(files)) => {
1162                then(host, files.into_iter().map(ChosenFile::from).collect(), cx)
1163            }
1164            Some(ContentsOutcome::Cancelled) => then(host, Vec::new(), cx),
1165            Some(ContentsOutcome::Failed(reason)) => Err(reason),
1166            None => Err(CHOOSER_GONE.into()),
1167        })
1168    }
1169
1170    /// Ask the person to choose a file with the host's chooser and run
1171    /// `then` on the UI thread with its path on this device, or `None` when
1172    /// they cancelled: for a file the app reads in place rather than into
1173    /// memory (a disk image, a video). Only a host whose files have paths
1174    /// answers; elsewhere use [`Self::choose_file`]. The effect must be
1175    /// declared `job`.
1176    pub fn choose_path(
1177        &mut self,
1178        request: PickRequest,
1179        then: impl FnOnce(&mut H, Option<std::path::PathBuf>, &mut Self) -> EffectAnswer + 'static,
1180    ) -> Effected {
1181        let chosen = request.send();
1182        self.wait_for(chosen, move |host, outcome, cx| match outcome {
1183            Some(PortalOutcome::Opened(path)) => then(host, Some(path), cx),
1184            Some(PortalOutcome::Cancelled) => then(host, None, cx),
1185            Some(PortalOutcome::Failed(reason)) => Err(reason),
1186            None => Err(CHOOSER_GONE.into()),
1187        })
1188    }
1189
1190    /// Ask the person where to save `bytes` with the host's chooser
1191    /// (`request` is a Save request suggesting a file name; a browser page
1192    /// downloads them under it), then run `then` on the UI thread with
1193    /// whether they were saved: `false` when the person cancelled. The
1194    /// effect must be declared `job`.
1195    pub fn save_file(
1196        &mut self,
1197        request: PickRequest,
1198        bytes: Vec<u8>,
1199        then: impl FnOnce(&mut H, bool, &mut Self) -> EffectAnswer + 'static,
1200    ) -> Effected {
1201        let saved = request.send_save_contents(bytes);
1202        self.wait_for(saved, move |host, outcome, cx| match outcome {
1203            Some(SaveOutcome::Saved) => then(host, true, cx),
1204            Some(SaveOutcome::Cancelled) => then(host, false, cx),
1205            Some(SaveOutcome::Failed(reason)) => Err(reason),
1206            None => Err(CHOOSER_GONE.into()),
1207        })
1208    }
1209
1210    /// Ask the person where to save with the host's chooser (`request` is
1211    /// a Save request suggesting a file name), then run `then` on the UI
1212    /// thread with the place, or `None` when they cancelled. The place takes
1213    /// bytes made after the choice, and can be kept to write there again on
1214    /// every later Save without asking: a path where files have paths, the
1215    /// browser's own Save dialog file on a browser page. The effect must be
1216    /// declared `job`.
1217    pub fn choose_destination(
1218        &mut self,
1219        request: PickRequest,
1220        then: impl FnOnce(&mut H, Option<SaveDestination>, &mut Self) -> EffectAnswer + 'static,
1221    ) -> Effected {
1222        let chosen = request.send_save_destination();
1223        self.wait_on(
1224            move || chosen.try_recv(),
1225            move |host, outcome, cx| match outcome {
1226                Some(DestinationOutcome::Chosen(destination)) => then(host, Some(destination), cx),
1227                Some(DestinationOutcome::Cancelled) => then(host, None, cx),
1228                Some(DestinationOutcome::Failed(reason)) => Err(reason),
1229                None => Err(CHOOSER_GONE.into()),
1230            },
1231        )
1232    }
1233
1234    /// Run `then` on the UI thread once `destination` holds `bytes`: `Ok`
1235    /// when they were saved, the reason when not. The effect must be
1236    /// declared `job`.
1237    pub fn write_destination(
1238        &mut self,
1239        destination: &SaveDestination,
1240        bytes: Vec<u8>,
1241        then: impl FnOnce(&mut H, Result<(), String>, &mut Self) -> EffectAnswer + 'static,
1242    ) -> Effected {
1243        let written = destination.write(bytes);
1244        self.wait_for(written, move |host, outcome, cx| match outcome {
1245            Some(SaveOutcome::Saved) => then(host, Ok(()), cx),
1246            Some(SaveOutcome::Cancelled) => then(host, Err("the save was cancelled".into()), cx),
1247            Some(SaveOutcome::Failed(reason)) => then(host, Err(reason), cx),
1248            None => Err(CHOOSER_GONE.into()),
1249        })
1250    }
1251
1252    /// Ask the host to show what a launch naming `arguments` opens in this
1253    /// window, in place of the record it shows ([`OpenHere::open`](super::OpenHere::open)), then
1254    /// run `then` on the UI thread with its answer: shown, or why not. The
1255    /// answer arrives even when the window shows the new record before this
1256    /// surface draws another frame. Cancelling the effect withdraws a
1257    /// request the host has not taken up yet. The effect must be declared
1258    /// `job`.
1259    pub fn open_here(
1260        &mut self,
1261        opener: &super::OpenHere,
1262        arguments: &[String],
1263        then: impl FnOnce(&mut H, Result<(), String>, &mut Self) -> EffectAnswer + 'static,
1264    ) -> Effected {
1265        /// Withdraws the request when the job stops unanswered.
1266        struct Asked(ui_core::application::ReplacementOutcome);
1267        impl Drop for Asked {
1268            fn drop(&mut self) {
1269                self.0.withdraw();
1270            }
1271        }
1272        let asked = Asked(opener.open(arguments));
1273        let progress = self.track(Some(self.then(then)));
1274        if let Some(started) = self.inv.started.as_mut() {
1275            started.core.source = Some(Box::new(move || {
1276                // The job's own channel stays open while it waits here.
1277                let _waiting = &progress;
1278                asked
1279                    .0
1280                    .get()
1281                    .map(|answer| Box::new(answer) as Box<dyn Any + Send>)
1282            }));
1283        }
1284        Effected::Pending
1285    }
1286}
1287
1288/// Work a view or surface starts on its own, with nobody invoking an
1289/// effect: a route fetched when the destination changes, a feed refreshed
1290/// when it opens. Each job runs under a key; starting another under a key
1291/// still running stops the earlier one, so the latest request wins. A job
1292/// is started like an effect's ([`EffectCx::thread`], [`EffectCx::spawn`])
1293/// and finishes the same way, on the UI thread, with ordinary actions; a
1294/// surface's completion gets `&mut` the surface, and a window's the window
1295/// ([`ViewCx::window_jobs`](super::ViewCx::window_jobs)). Jobs run only in a
1296/// window someone uses: a past version and a capture start none.
1297///
1298/// Get one from [`SurfaceCx::jobs`](super::SurfaceCx::jobs) or
1299/// [`ViewCx::jobs`](super::ViewCx::jobs). Clones share the jobs.
1300pub struct Jobs<R: Resource> {
1301    env: Option<EffectEnv<R>>,
1302    queue: Rc<RefCell<JobQueue>>,
1303}
1304
1305impl<R: Resource> Clone for Jobs<R> {
1306    fn clone(&self) -> Self {
1307        Self {
1308            env: self.env.clone(),
1309            queue: Rc::clone(&self.queue),
1310        }
1311    }
1312}
1313
1314/// Where a running job last said it is, as its [`Progress`] reported it.
1315#[derive(Clone, Debug, PartialEq, Eq)]
1316pub struct JobProgress {
1317    pub phase: String,
1318    pub completed: Option<u64>,
1319    pub total: Option<u64>,
1320}
1321
1322/// What the jobs' owner has yet to take up.
1323#[derive(Default)]
1324struct JobQueue {
1325    /// Jobs started and not yet tracked.
1326    started: Vec<(String, Job)>,
1327    cancelled: Vec<String>,
1328    running: std::collections::HashSet<String>,
1329    /// The last progress each running job reported.
1330    progress: HashMap<String, JobProgress>,
1331}
1332
1333impl<R: Resource> Jobs<R> {
1334    pub(crate) fn new(env: Option<EffectEnv<R>>) -> Self {
1335        Self {
1336            env,
1337            queue: JobQueue::shared(),
1338        }
1339    }
1340
1341    /// Start the job `key` as `start` does it, through an [`EffectCx`] whose
1342    /// completions get `&mut H`: the surface itself for a surface's jobs
1343    /// (`jobs.start::<Self>(…)`), the window for a window's
1344    /// ([`ViewCx::window_jobs`](super::ViewCx::window_jobs)), `()` for a
1345    /// view's. Work it finishes at once needs no job; a failure is logged,
1346    /// as nobody waits for it.
1347    pub fn start<H: 'static>(
1348        &self,
1349        key: impl Into<String>,
1350        start: impl FnOnce(&mut EffectCx<R, H>) -> EffectAnswer,
1351    ) {
1352        let Some(env) = &self.env else {
1353            return;
1354        };
1355        if !env.active() {
1356            return;
1357        }
1358        let mut cx = EffectCx::<R, H>::new(env.clone(), None);
1359        let answer = start(&mut cx);
1360        self.queue
1361            .borrow_mut()
1362            .started(key.into(), answer, cx.inv.started.take(), R::KIND);
1363    }
1364
1365    /// Stop the job `key`; its result, if it comes, is dropped.
1366    pub fn cancel(&self, key: &str) {
1367        let mut queue = self.queue.borrow_mut();
1368        queue.started.retain(|(queued, _)| queued != key);
1369        queue.progress.remove(key);
1370        if queue.running.remove(key) {
1371            queue.cancelled.push(key.to_owned());
1372        }
1373    }
1374
1375    /// Whether the job `key` is still running.
1376    pub fn is_running(&self, key: &str) -> bool {
1377        self.queue.borrow().running.contains(key)
1378    }
1379
1380    /// Where the running job `key` last said it is, for the view to show
1381    /// and publish; `None` before its first report and once it ended.
1382    pub fn progress(&self, key: &str) -> Option<JobProgress> {
1383        self.queue.borrow().progress.get(key).cloned()
1384    }
1385}
1386
1387/// The jobs one owner runs, polled where its completions get `&mut H`.
1388pub(crate) struct OwnedJobs<R: Resource, H> {
1389    jobs: Jobs<R>,
1390    running: Running<String>,
1391    marker: std::marker::PhantomData<fn(&mut H)>,
1392}
1393
1394impl<R: Resource, H: 'static> OwnedJobs<R, H> {
1395    pub(crate) fn new(jobs: Jobs<R>) -> Self {
1396        Self {
1397            jobs,
1398            running: Running::default(),
1399            marker: std::marker::PhantomData,
1400        }
1401    }
1402
1403    /// Take up the jobs started since, stop the cancelled ones, and finish
1404    /// those that are done with `host`.
1405    pub(crate) fn poll(&mut self, host: &mut H) {
1406        let Some(env) = self.jobs.env.clone() else {
1407            return;
1408        };
1409        poll_owned(
1410            &self.jobs.queue,
1411            &mut self.running,
1412            std::any::TypeId::of::<Then<R, H>>(),
1413            &mut |then, value, custody, invocation| {
1414                complete::<R, H>(&env, host, then, value, custody, invocation)
1415            },
1416        );
1417    }
1418}
1419
1420/// [`OwnedJobs::poll`] of the jobs of `owner` in `queue`, running in
1421/// `running`, their completions run by `complete`.
1422fn poll_owned(
1423    queue: &RefCell<JobQueue>,
1424    running: &mut Running<String>,
1425    owner: std::any::TypeId,
1426    complete: &mut Complete<'_>,
1427) {
1428    {
1429        let mut queue = queue.borrow_mut();
1430        for key in std::mem::take(&mut queue.cancelled) {
1431            running.cancel(&key);
1432        }
1433        let mut others = Vec::new();
1434        for (key, started) in std::mem::take(&mut queue.started) {
1435            if started.owner == owner {
1436                running.insert(key, started);
1437            } else {
1438                others.push((key, started));
1439            }
1440        }
1441        queue.started = others;
1442    }
1443    for key in running.keys() {
1444        while let Some(polled) = running.poll(&key, complete, None) {
1445            if !queue.borrow_mut().note(&key, polled) {
1446                break;
1447            }
1448        }
1449    }
1450}
1451
1452impl JobQueue {
1453    /// An empty queue, shared: made here once rather than in every app's
1454    /// [`Jobs`].
1455    #[inline(never)]
1456    fn shared() -> Rc<RefCell<Self>> {
1457        Rc::default()
1458    }
1459
1460    /// Queue the job `key` a start answered `answer` for, when it runs on;
1461    /// stop what it started otherwise. A job still queued under `key` is
1462    /// replaced.
1463    fn started(&mut self, key: String, answer: EffectAnswer, started: Option<Job>, kind: &str) {
1464        match (answer, started) {
1465            (Ok(Effected::Pending), Some(started)) => {
1466                self.cancelled.retain(|cancelled| *cancelled != key);
1467                self.started.retain(|(queued, _)| *queued != key);
1468                self.running.insert(key.clone());
1469                self.progress.remove(&key);
1470                self.started.push((key, started));
1471            }
1472            (answer, started) => {
1473                if let Some(started) = started {
1474                    started.stop();
1475                }
1476                if let Err(error) = answer {
1477                    tracing::warn!(%error, kind, %key, "a job failed to start");
1478                }
1479            }
1480        }
1481    }
1482
1483    /// Note what the job `key` reported; whether to poll it again.
1484    fn note(&mut self, key: &str, polled: Polled) -> bool {
1485        match polled {
1486            Polled::Progress {
1487                phase,
1488                completed,
1489                total,
1490                ..
1491            } => {
1492                self.progress.insert(
1493                    key.to_owned(),
1494                    JobProgress {
1495                        phase,
1496                        completed,
1497                        total,
1498                    },
1499                );
1500                true
1501            }
1502            Polled::Finished(None) => false,
1503            Polled::Finished(Some(result)) => {
1504                if let Err(error) = result {
1505                    tracing::warn!(error = %error.message, %key, "a job failed");
1506                }
1507                self.running.remove(key);
1508                self.progress.remove(key);
1509                false
1510            }
1511        }
1512    }
1513}
1514
1515/// What an effect, or one of the resource's own actions, looks like to
1516/// callers right now, when it differs from its declaration: unavailable
1517/// while the engine is offline, labelled "Stop recording" while recording.
1518#[derive(Clone, Debug, PartialEq)]
1519pub struct EffectStatus {
1520    id: &'static str,
1521    availability: Option<Availability>,
1522    label: Option<String>,
1523}
1524
1525impl EffectStatus {
1526    /// `effect` cannot be invoked now: `code` names why for programs,
1527    /// `reason` for people.
1528    pub fn unavailable<E, A>(
1529        effect: ActionRef<E, A>,
1530        code: impl Into<String>,
1531        reason: impl Into<String>,
1532    ) -> Self {
1533        Self {
1534            id: effect.id,
1535            availability: Some(Availability::Unavailable {
1536                code: code.into(),
1537                reason: reason.into(),
1538            }),
1539            label: None,
1540        }
1541    }
1542
1543    /// `effect` is offered as `label` now.
1544    pub fn labelled<E, A>(effect: ActionRef<E, A>, label: impl Into<String>) -> Self {
1545        Self {
1546            id: effect.id,
1547            availability: None,
1548            label: Some(label.into()),
1549        }
1550    }
1551
1552    /// Offer the effect as `label` too.
1553    #[must_use]
1554    pub fn with_label(mut self, label: impl Into<String>) -> Self {
1555        self.label = Some(label.into());
1556        self
1557    }
1558
1559    /// The id of the action or effect this status is about.
1560    pub(crate) fn id(&self) -> &'static str {
1561        self.id
1562    }
1563
1564    /// Show `spec` as this status says it is right now.
1565    pub(crate) fn overlay(&self, spec: &mut ActionSpec) {
1566        if let Some(availability) = &self.availability {
1567            spec.type_mut().contract.availability = availability.clone();
1568        }
1569        if let Some(label) = &self.label {
1570            spec.type_mut().label.clone_from(label);
1571        }
1572    }
1573}
1574
1575/// The specs last built for one vocabulary, as callers saw them then: the
1576/// locale and statuses they were built at, kept so a publication that finds
1577/// both unchanged copies them instead of building every declaration again.
1578pub(crate) type KeptSpecs = std::cell::RefCell<Option<(u64, Vec<EffectStatus>, Vec<ActionSpec>)>>;
1579
1580/// The specs of `E` as callers see them at `statuses`, taken from `kept`
1581/// while the locale and the statuses are those it was built at.
1582pub(crate) fn kept_specs<E: Effect>(
1583    kept: &KeptSpecs,
1584    statuses: &[EffectStatus],
1585) -> Vec<ActionSpec> {
1586    kept_specs_of(kept, statuses, E::specs)
1587}
1588
1589/// [`kept_specs`] of the effects whose specs `specs` builds: compiled once
1590/// rather than for every effect vocabulary.
1591#[inline(never)]
1592pub(crate) fn kept_specs_of(
1593    kept: &KeptSpecs,
1594    statuses: &[EffectStatus],
1595    specs: fn() -> Vec<ActionSpec>,
1596) -> Vec<ActionSpec> {
1597    let locale = ui_core::i18n::I18nManager::revision();
1598    if let Some((_, _, specs)) = kept
1599        .borrow()
1600        .as_ref()
1601        .filter(|(at, was, _)| *at == locale && was.as_slice() == statuses)
1602    {
1603        return specs.clone();
1604    }
1605    let specs = overlaid(specs(), statuses);
1606    *kept.borrow_mut() = Some((locale, statuses.to_vec(), specs.clone()));
1607    specs
1608}
1609
1610/// `specs` with what `statuses` say of each laid over it, and unavailable
1611/// where they need what this host lacks: compiled once rather than for
1612/// every effect vocabulary.
1613#[inline(never)]
1614fn overlaid(specs: Vec<ActionSpec>, statuses: &[EffectStatus]) -> Vec<ActionSpec> {
1615    specs
1616        .into_iter()
1617        .map(|mut spec| {
1618            let id = spec.id.clone();
1619            for status in statuses.iter().filter(|status| status.id == id) {
1620                status.overlay(&mut spec);
1621            }
1622            require_host(&mut spec, host_lacks);
1623            spec
1624        })
1625        .collect()
1626}
1627
1628/// `spec` unavailable when one of its [`tilo_actions::REQUIRES_HOST_PREFIX`]
1629/// requirements is something `lacks` gives the host's reason for lacking.
1630fn require_host(spec: &mut ActionSpec, lacks: impl Fn(&str) -> Option<&'static str>) {
1631    if let Some(reason) = spec.contract.tags.iter().find_map(|tag| lacks(tag)) {
1632        spec.type_mut().contract.availability = Availability::Unavailable {
1633            code: tilo_actions::HOST_UNSUPPORTED_CODE.to_owned(),
1634            reason: reason.to_owned(),
1635        };
1636    }
1637}
1638
1639/// Why this host cannot meet `tag`, when it is a host requirement it lacks.
1640fn host_lacks(tag: &str) -> Option<&'static str> {
1641    tag.strip_prefix(tilo_actions::REQUIRES_HOST_PREFIX)?;
1642    let feature = match tag {
1643        tilo_actions::REQUIRES_NATIVE_PATHS_TAG => tilo_files::Feature::NativePaths,
1644        _ => return Some("This device cannot do this."),
1645    };
1646    match tilo_files::availability(feature) {
1647        tilo_files::Availability::Unsupported(reason) => Some(reason),
1648        tilo_files::Availability::Available | tilo_files::Availability::Prompt => None,
1649    }
1650}
1651
1652/// What a job reported when polled.
1653enum Polled {
1654    Progress {
1655        sequence: u64,
1656        phase: String,
1657        completed: Option<u64>,
1658        total: Option<u64>,
1659    },
1660    /// Finished, or chained into another job (`None`).
1661    Finished(Option<Result<ActionOutput, ActionFailure>>),
1662}
1663
1664/// The jobs one handler runs, keyed by what they answer.
1665pub(crate) struct Running<K: JobKey = InvocationId> {
1666    running: HashMap<K, Job>,
1667}
1668
1669impl<K: JobKey> Default for Running<K> {
1670    fn default() -> Self {
1671        K::running()
1672    }
1673}
1674
1675impl<K: JobKey> Drop for Running<K> {
1676    fn drop(&mut self) {
1677        K::stop_all(self);
1678    }
1679}
1680
1681/// What jobs are keyed by: the invocation an effect's job answers, the key
1682/// a view's job runs under, or the outbox entry a projection's job carries
1683/// out. What does not depend on the key's type otherwise is compiled once
1684/// for each of them here, rather than in every app that holds jobs.
1685pub(crate) trait JobKey: Eq + Hash + Clone + Sized {
1686    /// No jobs.
1687    fn running() -> Running<Self>;
1688    /// Stop every job in `running`.
1689    fn stop_all(running: &mut Running<Self>);
1690}
1691
1692macro_rules! job_key {
1693    ($($key:ty),*) => {$(
1694        impl JobKey for $key {
1695            #[inline(never)]
1696            fn running() -> Running<Self> {
1697                Running {
1698                    running: HashMap::new(),
1699                }
1700            }
1701
1702            #[inline(never)]
1703            fn stop_all(running: &mut Running<Self>) {
1704                for (_, job) in running.running.drain() {
1705                    job.stop();
1706                }
1707            }
1708        }
1709    )*};
1710}
1711
1712job_key!(InvocationId, String, Id);
1713
1714impl<K: JobKey> Running<K> {
1715    fn insert(&mut self, key: K, started: Job) {
1716        if let Some(previous) = self.running.insert(key, started) {
1717            previous.stop();
1718        }
1719    }
1720
1721    fn contains(&self, key: &K) -> bool {
1722        self.running.contains_key(key)
1723    }
1724
1725    fn keys(&self) -> Vec<K> {
1726        self.running.keys().cloned().collect()
1727    }
1728
1729    /// Stop the job `key` before it is polled when its caller cancelled it,
1730    /// or when a signal it answers to went off; `true` when it stopped.
1731    fn prepare(&mut self, key: &K, cancel: bool) -> bool {
1732        let stopped = self
1733            .running
1734            .get(key)
1735            .is_some_and(|job| cancel || job.core.fired());
1736        if stopped {
1737            self.cancel(key);
1738        }
1739        stopped
1740    }
1741
1742    /// Stop the job answering `key`; its result, if it comes, is dropped.
1743    fn cancel(&mut self, key: &K) -> bool {
1744        self.running.remove(key).map(Job::stop).is_some()
1745    }
1746
1747    /// The next thing the job `key` reports, running its completion on this
1748    /// thread when it finished.
1749    fn poll(
1750        &mut self,
1751        key: &K,
1752        complete: &mut Complete<'_>,
1753        invocation: Option<InvocationId>,
1754    ) -> Option<Polled> {
1755        let value = match self.running.get_mut(key)?.core.next()? {
1756            Next::Progress(progress) => return Some(progress),
1757            Next::Failed(error) => {
1758                self.cancel(key);
1759                return Some(Polled::Finished(Some(Err(error))));
1760            }
1761            Next::Finished(value) => value,
1762        };
1763        let job = self.running.remove(key)?;
1764        let Some(then) = job.then else {
1765            return Some(answered(value));
1766        };
1767        let (answer, next) = match complete(then, value, job.core.custody, invocation) {
1768            Ok(completed) => completed,
1769            Err(error) => return Some(Polled::Finished(Some(Err(failure(error))))),
1770        };
1771        let chained = matches!(answer, Ok(Effected::Pending)) && next.is_some();
1772        match next {
1773            Some(next) if chained => self.insert(key.clone(), next),
1774            Some(next) => next.stop(),
1775            None => {}
1776        }
1777        Some(Polled::Finished(completed(answer, chained)))
1778    }
1779}
1780
1781thread_local! {
1782    /// The effect invocation whose handler or completion runs on this
1783    /// thread now.
1784    static ACTING_FOR: Cell<Option<InvocationId>> = const { Cell::new(None) };
1785}
1786
1787/// The effect invocation whose handler or completion is running: what a
1788/// dispatcher queues now continues it, so the host applies it for the
1789/// effect's invoker.
1790pub(crate) fn acting_for() -> Option<InvocationId> {
1791    ACTING_FOR.get()
1792}
1793
1794/// Marks an effect invocation's handler or completion as running, until
1795/// dropped.
1796struct ActingFor(Option<InvocationId>);
1797
1798impl ActingFor {
1799    fn enter(invocation: Option<InvocationId>) -> Self {
1800        Self(ACTING_FOR.replace(invocation))
1801    }
1802}
1803
1804impl Drop for ActingFor {
1805    fn drop(&mut self) {
1806        ACTING_FOR.set(self.0);
1807    }
1808}
1809
1810/// What `called`, a call of another resource's `action`, came to once it
1811/// ended: its output, or why it did not go ahead. `None` while it runs.
1812fn called_outcome(
1813    called: &ui_core::InvocationCall,
1814    action: &str,
1815) -> Option<Result<ActionOutput, String>> {
1816    called.with(|arrival| match arrival {
1817        Arrival::Ready(InvocationUpdate::Succeeded { output, .. }) => Some(Ok(output.clone())),
1818        Arrival::Ready(InvocationUpdate::Failed { error, .. }) => {
1819            Some(Err(format!("{action}: {}", error.message)))
1820        }
1821        Arrival::Ready(InvocationUpdate::Cancelled { .. }) => {
1822            Some(Err(format!("{action} was cancelled")))
1823        }
1824        Arrival::Failed(error) => Some(Err(format!("{action}: {error}"))),
1825        Arrival::Pending { .. }
1826        | Arrival::Ready(
1827            InvocationUpdate::Accepted { .. }
1828            | InvocationUpdate::Progress { .. }
1829            | InvocationUpdate::Item { .. },
1830        ) => None,
1831    })
1832}
1833
1834/// An effect's failure told only by its message.
1835fn failure(message: String) -> ActionFailure {
1836    ActionFailure {
1837        code: "effect_failed".into(),
1838        message,
1839        details: None,
1840        retryable: false,
1841    }
1842}
1843
1844fn failed(invocation: InvocationId, error: ActionFailure) -> InvocationUpdate {
1845    InvocationUpdate::Failed { invocation, error }
1846}
1847
1848/// The handler of one effect vocabulary `E`, and the jobs it runs.
1849pub(crate) struct EffectRunner<R: Resource, H, E> {
1850    env: EffectEnv<R>,
1851    jobs: Running,
1852    marker: std::marker::PhantomData<fn(E, &mut H)>,
1853}
1854
1855impl<R: Resource, H: 'static, E: Effect> EffectRunner<R, H, E> {
1856    pub(crate) fn new(env: EffectEnv<R>) -> Self {
1857        Self {
1858            env,
1859            jobs: Running::default(),
1860            marker: std::marker::PhantomData,
1861        }
1862    }
1863
1864    /// Answer `op` when it is one of `E`'s effects: decode it, hand it to
1865    /// `handle`, and start what it started. `Ok(None)` for any other
1866    /// operation.
1867    pub(crate) fn start(
1868        &mut self,
1869        op: &ResourceOperation<'_>,
1870        host: &mut H,
1871        doc: Option<&tilo_resource::Doc>,
1872        handle: impl FnOnce(&mut H, E, &mut EffectCx<R, H>) -> EffectAnswer,
1873    ) -> Result<Option<ResourceOperationStart>, String> {
1874        let (request, custody) = op.with_custody();
1875        if request.type_id != E::OPERATION {
1876            return Ok(None);
1877        }
1878        op.check()?;
1879        let effect = E::from_payload(request.payload).map_err(|error| error.to_string())?;
1880        let id = effect.id();
1881        let declaration = E::declaration(id);
1882        let place = declaration
1883            .as_ref()
1884            .and_then(|decl| place_of(decl, &effect));
1885        let job = admitted(declaration.as_ref(), id, request.kind)?;
1886        let parts = invocation_parts(op, doc, place.clone())?;
1887        let mut cx = EffectCx::invoked(self.env.clone(), parts);
1888        let mut handling = Some((handle, effect));
1889        let answer = run_under(
1890            Some(request.invocation),
1891            custody,
1892            &mut || match handling.take() {
1893                Some((handle, effect)) => handle(host, effect, &mut cx),
1894                None => Err("an effect's handler ran twice".into()),
1895            },
1896        );
1897        cx.inv.document = None;
1898        let live = &self.env.live;
1899        finish_start(
1900            &mut self.jobs,
1901            Answered {
1902                id,
1903                invocation: request.invocation,
1904                job,
1905                answer,
1906                started: cx.inv.started.take(),
1907            },
1908            Tracking {
1909                started: &mut cx.inv.started,
1910                wake: &cx.env.wake,
1911                worker: cx.inv.worker_custody.as_ref(),
1912                custody: cx.inv.custody.as_ref(),
1913                scope: cx.env.scope.as_ref(),
1914                cancel_by: cx.inv.cancel_by.as_ref(),
1915                owner: std::any::TypeId::of::<Then<R, H>>(),
1916            },
1917            place.as_ref(),
1918            &|participant, place| live.land(participant, place),
1919        )
1920    }
1921
1922    /// The next update of the job answering `invocation`, if this runner
1923    /// runs it.
1924    pub(crate) fn poll(
1925        &mut self,
1926        invocation: InvocationId,
1927        cancel: bool,
1928        host: &mut H,
1929    ) -> Option<InvocationUpdate> {
1930        let env = &self.env;
1931        poll_invocation(
1932            &mut self.jobs,
1933            invocation,
1934            cancel,
1935            &mut |then, value, custody, invocation| {
1936                complete::<R, H>(env, host, then, value, custody, invocation)
1937            },
1938        )
1939    }
1940}
1941
1942/// The scope a worker runs under for `custody`, and the custody the
1943/// effect's dispatcher keeps.
1944fn custody_scopes(
1945    custody: Option<&ResourceInvocationScope>,
1946) -> Result<(Option<ResourceEffectScope>, Option<ResourceInvocationScope>), String> {
1947    let Some(custody) = custody else {
1948        return Ok((None, None));
1949    };
1950    Ok((Some(custody.effect_scope()?), Some(custody.clone())))
1951}
1952
1953/// A snapshot of `doc` to lend an effect's handler, and its heads.
1954fn lend(
1955    doc: Option<&tilo_resource::Doc>,
1956) -> (
1957    Option<Rc<RefCell<tilo_resource::Doc>>>,
1958    Vec<ui_core::automerge::ChangeHash>,
1959) {
1960    match doc {
1961        Some(doc) => (Some(Rc::new(RefCell::new(doc.clone()))), doc.base_heads()),
1962        None => (None, Vec::new()),
1963    }
1964}
1965
1966/// Whether an effect invoked as `kind` with `declaration` is admitted, and
1967/// whether it is a job.
1968///
1969/// # Errors
1970/// A view only changes what its caller sees and is admitted as one; an
1971/// effect invoked as a view would skip the checks an effect gets.
1972fn admitted(
1973    declaration: Option<&tilo_resource::effect::EffectDecl>,
1974    id: &str,
1975    kind: tilo_actions::OperationKind,
1976) -> Result<bool, String> {
1977    if declaration.map(|decl| decl.kind) != Some(kind) {
1978        return Err(format!("`{id}` was invoked as another kind of operation"));
1979    }
1980    Ok(declaration.is_some_and(tilo_resource::effect::EffectDecl::is_job))
1981}
1982
1983/// What an effect's handler answered.
1984struct Answered<'a> {
1985    id: &'a str,
1986    invocation: InvocationId,
1987    /// Whether the effect is declared a job.
1988    job: bool,
1989    answer: EffectAnswer,
1990    /// The job the handler started, if any.
1991    started: Option<Job>,
1992}
1993
1994/// Start what an effect's handler answered: its output at once, or the job
1995/// it started in `jobs`. `reply` tracks a job for an output a job's caller
1996/// learns as the job's; `land` moves the participant where the effect was
1997/// told to act once it succeeded.
1998fn settle_start(
1999    jobs: &mut Running,
2000    answered: Answered<'_>,
2001    reply: &mut dyn FnMut() -> Progress,
2002    land: &mut dyn FnMut(tilo_actions::ParticipantId),
2003) -> Result<ResourceOperationStart, String> {
2004    let Answered {
2005        id,
2006        invocation,
2007        job,
2008        answer,
2009        started,
2010    } = answered;
2011    let answer = match answer {
2012        Ok(answer) => answer,
2013        Err(error) => {
2014            if let Some(started) = started {
2015                started.stop();
2016            }
2017            return Err(error);
2018        }
2019    };
2020    // A participant told where to act is there now.
2021    if let Some(participant) = ui_core::invocation_scope::current_participant() {
2022        land(participant);
2023    }
2024    match (answer, started, job) {
2025        (Effected::Done(output), started, false) => {
2026            if let Some(started) = started {
2027                started.stop();
2028            }
2029            Ok(ResourceOperationStart::Completed(output))
2030        }
2031        // A job's caller learns its result as the job's, even when it
2032        // was ready at once.
2033        (Effected::Done(output), started, true) => {
2034            if let Some(started) = started {
2035                started.stop();
2036            }
2037            let reply = EffectReply {
2038                progress: reply(),
2039                completion: false,
2040            };
2041            reply.finish(Ok(output));
2042            Ok(ResourceOperationStart::Accepted)
2043        }
2044        (Effected::Pending, Some(started), true) => {
2045            jobs.insert(invocation, started);
2046            Ok(ResourceOperationStart::Accepted)
2047        }
2048        (Effected::Pending, Some(started), false) => {
2049            started.stop();
2050            Err(format!(
2051                "`{id}` is declared to answer at once; declare it `#[effect(job)]` to answer later"
2052            ))
2053        }
2054        (Effected::Pending, None, _) => Err(format!("`{id}` answered later but started no job")),
2055    }
2056}
2057
2058/// What an invoked effect's context holds besides its resource, made
2059/// outside the generic code so it is compiled once.
2060struct InvocationParts {
2061    inv: Answering,
2062    /// The custody the effect's dispatcher keeps, when it has one.
2063    lent: Option<ResourceInvocationScope>,
2064}
2065
2066/// The parts of the context of an effect invoked by `op`, told to act at
2067/// `place`, reading `doc`.
2068#[inline(never)]
2069fn invocation_parts(
2070    op: &ResourceOperation<'_>,
2071    doc: Option<&tilo_resource::Doc>,
2072    place: Option<tilo_resource::Place>,
2073) -> Result<InvocationParts, String> {
2074    let (request, custody) = op.with_custody();
2075    let custody = owned_scope(custody);
2076    let (worker_custody, lent) = custody_scopes(custody.as_ref())?;
2077    let delivered = custody
2078        .as_ref()
2079        .map(ResourceInvocationScope::invocation)
2080        .or_else(ui_core::invocation_scope::delivered_invocation)
2081        .filter(|delivered| *delivered == request.invocation);
2082    // Keep an immutable snapshot for this effect invocation, including any
2083    // pending edits. The effect may also read a historical version; neither
2084    // read borrows the author's document mutably.
2085    let (document, heads) = lend(doc);
2086    Ok(InvocationParts {
2087        inv: Answering {
2088            invocation: Some(request.invocation),
2089            started: None,
2090            document,
2091            heads,
2092            custody,
2093            worker_custody,
2094            delivered,
2095            target: op.target().cloned(),
2096            place,
2097            cancel_by: None,
2098        },
2099        lent,
2100    })
2101}
2102
2103/// The parts of the context of a completion or an outbox entry run for
2104/// `invocation`, under `custody`: no document, target or place.
2105#[inline(never)]
2106fn custody_parts(
2107    invocation: Option<InvocationId>,
2108    custody: Option<ResourceInvocationScope>,
2109) -> Result<InvocationParts, String> {
2110    let (worker_custody, lent) = custody_scopes(custody.as_ref())?;
2111    let mut inv = Answering::of(invocation);
2112    inv.custody = custody;
2113    inv.worker_custody = worker_custody;
2114    Ok(InvocationParts { inv, lent })
2115}
2116
2117/// Start in `jobs` what an effect's handler answered ([`settle_start`]),
2118/// and move the participant to `place` through `land` once it succeeded.
2119///
2120/// # Errors
2121/// As [`settle_start`].
2122#[inline(never)]
2123fn finish_start(
2124    jobs: &mut Running,
2125    answered: Answered<'_>,
2126    tracking: Tracking<'_>,
2127    place: Option<&tilo_resource::Place>,
2128    land: &dyn Fn(tilo_actions::ParticipantId, tilo_resource::Place),
2129) -> Result<Option<ResourceOperationStart>, String> {
2130    let invocation = answered.invocation;
2131    let Tracking {
2132        started,
2133        wake,
2134        worker,
2135        custody,
2136        scope,
2137        cancel_by,
2138        owner,
2139    } = tracking;
2140    let settled = settle_start(
2141        jobs,
2142        answered,
2143        &mut || {
2144            track_for(
2145                &mut Tracking {
2146                    started: &mut *started,
2147                    wake,
2148                    worker,
2149                    custody,
2150                    scope,
2151                    cancel_by,
2152                    owner,
2153                },
2154                None,
2155            )
2156        },
2157        &mut |participant| {
2158            if let Some(place) = place {
2159                land(participant, place.clone());
2160            }
2161        },
2162    );
2163    if let Some(ready) = started.take() {
2164        jobs.insert(invocation, ready);
2165    }
2166    settled.map(Some)
2167}
2168
2169/// The next update of the job in `jobs` answering `invocation`, its
2170/// completion run by `complete`; `None` when it runs no such job or has
2171/// nothing new.
2172fn poll_invocation(
2173    jobs: &mut Running,
2174    invocation: InvocationId,
2175    cancel: bool,
2176    complete: &mut Complete<'_>,
2177) -> Option<InvocationUpdate> {
2178    // A caller that stopped waiting, and a job left to answer past its
2179    // caller's wait once that caller gave up, both end here.
2180    if jobs.prepare(&invocation, cancel) {
2181        return Some(InvocationUpdate::Cancelled {
2182            invocation,
2183            message: None,
2184        });
2185    }
2186    match jobs.poll(&invocation, complete, Some(invocation))? {
2187        Polled::Progress {
2188            sequence,
2189            phase,
2190            completed,
2191            total,
2192        } => Some(InvocationUpdate::Progress {
2193            invocation,
2194            sequence,
2195            phase,
2196            completed,
2197            total,
2198            message: None,
2199        }),
2200        Polled::Finished(None) => None,
2201        Polled::Finished(Some(Ok(output))) => {
2202            Some(InvocationUpdate::Succeeded { invocation, output })
2203        }
2204        Polled::Finished(Some(Err(error))) => Some(failed(invocation, error)),
2205    }
2206}
2207
2208/// One registered effect vocabulary: what the root publishes for it.
2209struct Published {
2210    alive: Weak<()>,
2211    operation: &'static str,
2212    specs: Box<dyn Fn() -> Vec<ActionSpec>>,
2213}
2214
2215/// A view's effect handler, type-erased for the root.
2216trait ViewHandler {
2217    fn start(
2218        &mut self,
2219        op: &ResourceOperation<'_>,
2220        doc: &tilo_resource::Doc,
2221        view: &mut dyn ui_core::widget::Widget,
2222    ) -> Result<Option<ResourceOperationStart>, String>;
2223    fn poll(
2224        &mut self,
2225        invocation: InvocationId,
2226        cancel: bool,
2227        view: &mut dyn ui_core::widget::Widget,
2228    ) -> Option<InvocationUpdate>;
2229    /// Finish the jobs a window started on its own.
2230    fn poll_jobs(&mut self, _view: &mut dyn ui_core::widget::Widget) {}
2231}
2232
2233type Handle<R, E> = Box<dyn FnMut(E, &mut EffectCx<R>) -> EffectAnswer>;
2234
2235struct Handler<R: Resource, E> {
2236    runner: EffectRunner<R, (), E>,
2237    handle: Handle<R, E>,
2238}
2239
2240impl<R: Resource, E: Effect> ViewHandler for Handler<R, E> {
2241    fn start(
2242        &mut self,
2243        op: &ResourceOperation<'_>,
2244        doc: &tilo_resource::Doc,
2245        _view: &mut dyn ui_core::widget::Widget,
2246    ) -> Result<Option<ResourceOperationStart>, String> {
2247        let handle = &mut self.handle;
2248        // The handler reads the document as it stands
2249        // ([`EffectCx::document`]), as a hosted surface's does.
2250        self.runner
2251            .start(op, &mut (), Some(doc), |(), effect, cx| handle(effect, cx))
2252    }
2253
2254    fn poll(
2255        &mut self,
2256        invocation: InvocationId,
2257        cancel: bool,
2258        _view: &mut dyn ui_core::widget::Widget,
2259    ) -> Option<InvocationUpdate> {
2260        self.runner.poll(invocation, cancel, &mut ())
2261    }
2262}
2263
2264/// A registered projection drain ([`RecordEnv::outbox`](super::RecordEnv::outbox)),
2265/// as the view or surface that registered it holds it.
2266///
2267/// A drain looks at the document after every change. When what it compares
2268/// the document against lives outside — the configuration a service reports
2269/// back — tell it that changed with [`Self::outside_changed`], and on the
2270/// next frame it carries out again every entry it still lists.
2271#[derive(Clone, Default)]
2272pub struct Projection {
2273    /// `None` where drains never run: History, a capture.
2274    outside_changed: Option<Rc<Cell<bool>>>,
2275}
2276
2277impl Projection {
2278    /// The outside world the drain compares the document against changed:
2279    /// carry out what is still pending on the next frame, even what was
2280    /// carried out at this revision already.
2281    pub fn outside_changed(&self) {
2282        if let Some(flag) = &self.outside_changed {
2283            flag.set(true);
2284        }
2285    }
2286}
2287
2288impl std::fmt::Debug for Projection {
2289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2290        f.debug_struct("Projection")
2291            .field("runs", &self.outside_changed.is_some())
2292            .finish()
2293    }
2294}
2295
2296/// A projection drain, type-erased for the root.
2297trait Drainer {
2298    /// Start what is pending in `state` (the resource's state) and not
2299    /// running or just settled.
2300    fn run(&mut self, state: &dyn Any, revision: u64);
2301    /// Settle finished jobs at `revision`; whether any did.
2302    fn poll(&mut self, revision: u64) -> bool;
2303    /// Forget which entries finished, so those still pending run again.
2304    fn forget_settled(&mut self);
2305}
2306
2307/// The pending entries a resource state (as `&dyn Any`) holds that this
2308/// device owns, each boxed; `None` for the state of another resource.
2309type OwnedEntries = Box<dyn Fn(&dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>>>;
2310/// Run one entry (boxed) as an invocation under a custody: what it answered
2311/// and the job it started.
2312type StartEntry = Box<
2313    dyn FnMut(
2314        &Id,
2315        Box<dyn Any>,
2316        Option<InvocationId>,
2317        Option<&ResourceInvocationScope>,
2318    ) -> Result<(EffectAnswer, Option<Job>), String>,
2319>;
2320/// How an entry's handler answer settles it: the settle edit and what to
2321/// tell an invocation waiting on it, `None` when the outbox offers no settle
2322/// edit, or why the entry stays pending.
2323type SettleEntry = Box<
2324    dyn Fn(
2325        &Id,
2326        Result<ActionOutput, String>,
2327    ) -> Result<Option<(ApplySettlement, Result<ActionOutput, ActionFailure>)>, String>,
2328>;
2329
2330/// The drain of one [`Outbox`](tilo_resource::Outbox) on the device that
2331/// owns its entries ([`RecordEnv::outbox`](super::RecordEnv::outbox)). Only
2332/// reading, running and settling an entry know the resource and the entry's
2333/// types ([`Effects::outbox`] builds them); the bookkeeping is compiled once.
2334struct OutboxDrain {
2335    kind: &'static str,
2336    owned: OwnedEntries,
2337    start: StartEntry,
2338    settle: SettleEntry,
2339    /// Runs a job's completion, typed by the resource.
2340    complete: Box<Complete<'static>>,
2341    /// What an entry abandoned by its invocation settles with.
2342    abandoned: fn() -> Option<serde_json::Value>,
2343    jobs: Running<Id>,
2344    /// Entries whose run failed for now, and the revision it failed at: run
2345    /// again once the document moved on or the outside changed.
2346    retry: HashMap<Id, u64>,
2347    /// Entries this drain settled, never run again here while their
2348    /// settlement is on its way into the state.
2349    settled: std::collections::HashSet<Id>,
2350    /// Entries running for the local invocation whose action queued them,
2351    /// which completes once they settled.
2352    caused: HashMap<Id, (InvocationId, causal::Group)>,
2353    /// The revision the drain last looked at.
2354    revision: u64,
2355    /// Settlements to write in the next settle pass, shared with the root.
2356    settling: Settling,
2357}
2358
2359/// One outbox entry's settlement, written in the settle pass of the frame
2360/// that learned it: the settle edit, and the invocation that waits for it
2361/// with what to tell it once the edit is in the document.
2362struct Settlement {
2363    apply: ApplySettlement,
2364    waiting: Option<(causal::Group, Result<ActionOutput, ActionFailure>)>,
2365}
2366
2367type Settling = Rc<RefCell<Vec<Settlement>>>;
2368type ApplySettlement =
2369    Box<dyn FnOnce(&mut ui_core::widget::Edits<'_>) -> Result<(), tilo_actions::ActionError>>;
2370
2371/// Write the settlements in `settling`, then tell the invocations waiting on
2372/// them how they settled.
2373fn apply_settlements(settling: &Settling, edits: &mut ui_core::widget::Edits<'_>) {
2374    let settling = std::mem::take(&mut *settling.borrow_mut());
2375    for Settlement { apply, waiting } in settling {
2376        let applied = apply(edits);
2377        if let Some((group, told)) = waiting {
2378            group.borrow_mut().finished(match applied {
2379                Ok(()) => told,
2380                Err(error) => Err(failure(error.to_string())),
2381            });
2382        } else if let Err(error) = applied {
2383            tracing::warn!(%error, "an outbox settlement was refused");
2384        }
2385    }
2386}
2387
2388/// How a handler's answer settles its entry: a [`Settled`] it answered
2389/// ([`Effected::settled`]), the result `O` it answered, or nothing to report
2390/// (`O`'s default).
2391fn settlement<O>(output: &ActionOutput) -> Result<Settled<O>, String>
2392where
2393    O: serde::de::DeserializeOwned + Default,
2394{
2395    let Some(value) = &output.value else {
2396        return Ok(Settled::Done(O::default()));
2397    };
2398    serde_json::from_value::<Settled<O>>(value.clone())
2399        .or_else(|_| serde_json::from_value::<O>(value.clone()).map(Settled::Done))
2400        .map_err(|error| {
2401            format!("an outbox handler answered what its entry does not settle with: {error}")
2402        })
2403}
2404
2405/// What an outbox entry abandoned by whoever it was for settles with.
2406fn abandoned<O: serde::Serialize + Default>() -> Option<serde_json::Value> {
2407    serde_json::to_value(Settled::<O>::failed("cancelled")).ok()
2408}
2409
2410impl OutboxDrain {
2411    /// Record how the entry `id` settled, or leave it pending to run again;
2412    /// an invocation waiting on it learns which.
2413    fn finish(&mut self, id: &Id, answer: Result<ActionOutput, String>) -> bool {
2414        let group = self.caused.remove(id).map(|(_, group)| group);
2415        match (self.settle)(id, answer) {
2416            Ok(Some((apply, told))) => {
2417                self.settled.insert(id.clone());
2418                // The settlement is in the document before whoever waits on
2419                // the entry hears it settled: their next request sees it.
2420                self.settling.borrow_mut().push(Settlement {
2421                    apply,
2422                    waiting: group.map(|group| (group, told)),
2423                });
2424                true
2425            }
2426            Ok(None) => {
2427                self.settled.insert(id.clone());
2428                false
2429            }
2430            Err(error) => {
2431                tracing::warn!(%error, kind = self.kind, "an outbox entry stays pending");
2432                if let Some(group) = group {
2433                    group.borrow_mut().finished(Err(failure(error)));
2434                }
2435                self.retry.insert(id.clone(), self.revision);
2436                false
2437            }
2438        }
2439    }
2440
2441    /// The pending entries of `state` this device owns.
2442    fn owned(&self, state: &dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>> {
2443        (self.owned)(state)
2444    }
2445
2446    /// Run `value` (the entry `id`) as `invocation`, under its custody when
2447    /// it has one. Whether it finished at once.
2448    fn start_entry(
2449        &mut self,
2450        id: Id,
2451        value: Box<dyn Any>,
2452        invocation: Option<InvocationId>,
2453        custody: Option<&ResourceInvocationScope>,
2454    ) -> Result<(), String> {
2455        let (answer, started) = (self.start)(&id, value, invocation, custody)?;
2456        match (answer, started) {
2457            (Ok(Effected::Pending), Some(started)) => self.jobs.insert(id, started),
2458            (answer, started) => {
2459                if let Some(started) = started {
2460                    started.stop();
2461                }
2462                let answer = answer.and_then(|effected| match effected {
2463                    Effected::Done(output) => Ok(output),
2464                    Effected::Pending => {
2465                        Err("an outbox handler answered later without starting a job".into())
2466                    }
2467                });
2468                self.finish(&id, answer);
2469            }
2470        }
2471        Ok(())
2472    }
2473}
2474
2475impl Drainer for OutboxDrain {
2476    fn run(&mut self, state: &dyn Any, revision: u64) {
2477        let Some(pending) = self.owned(state) else {
2478            return;
2479        };
2480        self.revision = revision;
2481        self.settled
2482            .retain(|id| pending.iter().any(|(entry, _)| entry == id));
2483        self.retry
2484            .retain(|id, _| pending.iter().any(|(entry, _)| entry == id));
2485        for (id, value) in pending {
2486            if self.jobs.contains(&id)
2487                || self.settled.contains(&id)
2488                || self.retry.get(&id) == Some(&revision)
2489            {
2490                continue;
2491            }
2492            if let Err(error) = self.start_entry(id, value, None, None) {
2493                tracing::warn!(%error, kind = self.kind, "an outbox entry did not start");
2494            }
2495        }
2496    }
2497
2498    fn poll(&mut self, revision: u64) -> bool {
2499        self.revision = revision;
2500        let mut finished = false;
2501        for id in self.jobs.keys() {
2502            let invocation = self.caused.get(&id).map(|(invocation, _)| *invocation);
2503            while let Some(polled) = self.jobs.poll(&id, &mut *self.complete, invocation) {
2504                let Polled::Finished(result) = polled else {
2505                    continue;
2506                };
2507                if let Some(result) = result {
2508                    self.finish(&id, result.map_err(|error| error.message));
2509                    finished = true;
2510                }
2511                break;
2512            }
2513        }
2514        finished
2515    }
2516
2517    fn forget_settled(&mut self) {
2518        self.retry.clear();
2519    }
2520}
2521
2522/// A drain shared by the drains that run what is pending and, for
2523/// [`RecordEnv::outbox_awaited`](super::RecordEnv::outbox_awaited), by those that
2524/// run what an invocation queued.
2525impl<D: Drainer> Drainer for Rc<RefCell<D>> {
2526    fn run(&mut self, state: &dyn Any, revision: u64) {
2527        self.borrow_mut().run(state, revision);
2528    }
2529
2530    fn poll(&mut self, revision: u64) -> bool {
2531        self.borrow_mut().poll(revision)
2532    }
2533
2534    fn forget_settled(&mut self) {
2535        self.borrow_mut().forget_settled();
2536    }
2537}
2538
2539struct Registered {
2540    alive: Weak<()>,
2541    operation: &'static str,
2542    handler: Box<dyn ViewHandler>,
2543}
2544
2545struct Drains {
2546    alive: Weak<()>,
2547    drain: Box<dyn Drainer>,
2548}
2549
2550#[derive(Default)]
2551struct Inner {
2552    causal: causal::CausalEffects,
2553    published: Vec<Published>,
2554    handlers: Vec<Registered>,
2555    drains: Vec<Drains>,
2556    /// The revision drains last ran at.
2557    drained: Option<u64>,
2558    /// Set when the outside world a drain compares against changed, so the
2559    /// drains look again although the document did not move.
2560    outside_changed: Rc<Cell<bool>>,
2561    /// Outbox settlements waiting for the settle pass.
2562    settling: Settling,
2563}
2564
2565/// Every effect handler and projection drain of one live resource root, and
2566/// what they publish. Clones share it.
2567pub(crate) struct Effects<R: Resource> {
2568    env: Option<EffectEnv<R>>,
2569    inner: Rc<RefCell<Inner>>,
2570    /// The jobs the views start on their own, polled with the drains.
2571    jobs: Rc<RefCell<OwnedJobs<R, ()>>>,
2572    /// Whether views and surfaces start work on their own (jobs, projection
2573    /// drains), or only the effects someone invokes run.
2574    starts_work: bool,
2575}
2576
2577/// An empty [`Inner`], shared. Not generic over the resource, so its
2578/// construction is compiled once rather than in every app's `Effects`.
2579#[inline(never)]
2580fn new_inner() -> Rc<RefCell<Inner>> {
2581    Rc::default()
2582}
2583
2584impl<R: Resource> Clone for Effects<R> {
2585    fn clone(&self) -> Self {
2586        Self {
2587            env: self.env.clone(),
2588            inner: Rc::clone(&self.inner),
2589            jobs: Rc::clone(&self.jobs),
2590            starts_work: self.starts_work,
2591        }
2592    }
2593}
2594
2595impl<R: Resource> Effects<R> {
2596    /// Effects of the live app, which run.
2597    pub(crate) fn live(dispatch: Dispatcher<R>, live: Live<R>) -> Self {
2598        let env = EffectEnv {
2599            dispatch,
2600            live,
2601            wake: WakeHandle::new(),
2602            scope: None,
2603        };
2604        Self {
2605            jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(Some(env.clone()))))),
2606            env: Some(env),
2607            inner: new_inner(),
2608            starts_work: true,
2609        }
2610    }
2611
2612    /// Effects of a capture a headless drive invokes actions on: an effect
2613    /// it invokes runs, with the work it starts, as it would in a window
2614    /// someone uses; the views and surfaces start nothing on their own and
2615    /// no projection drains, so the scene stays the scenario's fixture.
2616    pub(crate) fn invoked(dispatch: Dispatcher<R>, live: Live<R>) -> Self {
2617        Self {
2618            env: Some(EffectEnv {
2619                dispatch,
2620                live,
2621                wake: WakeHandle::new(),
2622                scope: None,
2623            }),
2624            inner: new_inner(),
2625            jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(None)))),
2626            starts_work: false,
2627        }
2628    }
2629
2630    /// Effects of a past version, which never run: History shows what the
2631    /// document was, it does not do again what was done.
2632    pub(crate) fn inert() -> Self {
2633        Self {
2634            env: None,
2635            inner: new_inner(),
2636            jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(None)))),
2637            starts_work: false,
2638        }
2639    }
2640
2641    pub(crate) fn env(&self) -> Option<&EffectEnv<R>> {
2642        self.env.as_ref()
2643    }
2644
2645    /// The jobs the views start on their own.
2646    #[inline(never)]
2647    pub(crate) fn view_jobs(&self) -> Jobs<R> {
2648        self.jobs.borrow().jobs.clone()
2649    }
2650
2651    /// Jobs for one surface, whose completions get `&mut` the surface.
2652    #[inline(never)]
2653    pub(crate) fn surface_jobs(&self) -> Jobs<R> {
2654        Jobs::new(self.working_env().cloned())
2655    }
2656
2657    pub(crate) fn scoped_surface_jobs(&self, scope: &Rc<super::surface::SurfaceScope>) -> Jobs<R> {
2658        Jobs::new(self.working_env().map(|env| env.scoped(scope)))
2659    }
2660
2661    /// The environment work a view or surface starts on its own runs in;
2662    /// `None` where none runs.
2663    fn working_env(&self) -> Option<&EffectEnv<R>> {
2664        self.env.as_ref().filter(|_| self.starts_work)
2665    }
2666
2667    /// Take up and finish the views' jobs.
2668    #[inline(never)]
2669    pub(crate) fn poll_jobs(&self) {
2670        self.jobs.borrow_mut().poll(&mut ());
2671        self.inner.borrow_mut().causal.poll();
2672    }
2673
2674    /// Publish vocabulary `E` while `alive` lives, as `specs` describes it
2675    /// at each publication.
2676    pub(crate) fn publish<E: Effect>(
2677        &self,
2678        alive: Weak<()>,
2679        specs: impl Fn() -> Vec<ActionSpec> + 'static,
2680    ) {
2681        self.inner.borrow_mut().published.push(Published {
2682            alive,
2683            operation: E::OPERATION,
2684            specs: Box::new(specs),
2685        });
2686    }
2687
2688    pub(crate) fn handle<E: Effect>(
2689        &self,
2690        alive: Weak<()>,
2691        handle: impl FnMut(E, &mut EffectCx<R>) -> EffectAnswer + 'static,
2692    ) {
2693        let Some(env) = self.env.clone() else {
2694            return;
2695        };
2696        self.inner.borrow_mut().handlers.push(Registered {
2697            alive,
2698            operation: E::OPERATION,
2699            handler: Box::new(Handler {
2700                runner: EffectRunner::<R, (), E>::new(env),
2701                handle: Box::new(handle),
2702            }),
2703        });
2704    }
2705
2706    pub(crate) fn outbox<T, O>(
2707        &self,
2708        alive: Weak<()>,
2709        outbox: Path<R, Outbox<T, O>>,
2710        owns: impl Fn(&R::State, &T) -> bool + 'static,
2711        run: impl FnMut(&Id, T, &mut EffectCx<R>) -> EffectAnswer + 'static,
2712        awaited: bool,
2713    ) -> Projection
2714    where
2715        R: Resource<State = R> + tilo_resource::Model + 'static,
2716        T: tilo_resource::Model + Clone + 'static,
2717        O: serde::Serialize + serde::de::DeserializeOwned + Default + 'static,
2718    {
2719        let Some(env) = self.working_env().cloned() else {
2720            return Projection::default();
2721        };
2722        let reader = outbox.clone();
2723        let owned: OwnedEntries = Box::new(move |state: &dyn Any| {
2724            let state = state.downcast_ref::<R>()?;
2725            Some(reader.get(state).map_or_else(Vec::new, |outbox| {
2726                outbox
2727                    .pending()
2728                    .filter(|(_, value)| owns(state, value))
2729                    .map(|(id, value)| (id.clone(), Box::new(value.clone()) as Box<dyn Any>))
2730                    .collect()
2731            }))
2732        });
2733        let mut run = run;
2734        let starting = env.clone();
2735        let start: StartEntry = Box::new(move |id, value, invocation, custody| {
2736            let Ok(value) = value.downcast::<T>() else {
2737                return Err("an outbox entry is of another type".into());
2738            };
2739            let mut cx = EffectCx::invoked(
2740                starting.clone(),
2741                custody_parts(invocation, owned_scope(custody))?,
2742            );
2743            let mut value = Some(*value);
2744            let answer = run_under(invocation, custody, &mut || match value.take() {
2745                Some(value) => run(id, value, &mut cx),
2746                None => Err("an outbox entry ran twice".into()),
2747            });
2748            Ok((answer, cx.inv.started.take()))
2749        });
2750        let dispatch = env.dispatch.clone();
2751        let settle: SettleEntry = Box::new(move |id, answer| {
2752            let settled = answer.and_then(|output| settlement::<O>(&output))?;
2753            let told = match &settled {
2754                Settled::Done(_) => Ok(ActionOutput::default()),
2755                Settled::Failed { reason, .. } => Err(failure(reason.clone())),
2756            };
2757            let Some(action) = R::edit_action(outbox.settle(id, &settled)) else {
2758                return Ok(None);
2759            };
2760            let dispatch = dispatch.clone();
2761            let apply: ApplySettlement = Box::new(move |edits| dispatch.apply_now(edits, &action));
2762            Ok(Some((apply, told)))
2763        });
2764        let completing = env;
2765        let complete: Box<Complete<'static>> = Box::new(move |then, value, custody, invocation| {
2766            complete::<R, ()>(&completing, &mut (), then, value, custody, invocation)
2767        });
2768        let drain = Rc::new(RefCell::new(OutboxDrain {
2769            kind: R::KIND,
2770            owned,
2771            start,
2772            settle,
2773            complete,
2774            abandoned: abandoned::<O>,
2775            jobs: Running::default(),
2776            retry: HashMap::new(),
2777            settled: std::collections::HashSet::new(),
2778            caused: HashMap::new(),
2779            revision: 0,
2780            settling: Rc::clone(&self.inner.borrow().settling),
2781        }));
2782        let mut inner = self.inner.borrow_mut();
2783        if awaited {
2784            inner
2785                .causal
2786                .register_drainer(alive.clone(), Box::new(Rc::clone(&drain)));
2787        }
2788        inner.drains.push(Drains {
2789            alive,
2790            drain: Box::new(drain),
2791        });
2792        inner.drained = None;
2793        Projection {
2794            outside_changed: Some(Rc::clone(&inner.outside_changed)),
2795        }
2796    }
2797
2798    /// The operation types of every published vocabulary.
2799    pub(crate) fn operations(&self) -> Vec<&'static str> {
2800        self.inner.borrow_mut().operations()
2801    }
2802
2803    /// Every published effect, as callers see it now.
2804    #[inline(never)]
2805    pub(crate) fn specs(&self) -> Vec<ActionSpec> {
2806        self.inner.borrow_mut().specs()
2807    }
2808
2809    /// Answer `op` with the view handler of its vocabulary.
2810    pub(crate) fn start(
2811        &self,
2812        op: &ResourceOperation<'_>,
2813        doc: &tilo_resource::Doc,
2814        view: &mut dyn ui_core::widget::Widget,
2815    ) -> Result<Option<ResourceOperationStart>, String> {
2816        let operation = op.with_custody().0.type_id;
2817        self.with_handlers(|handlers| {
2818            for entry in handlers {
2819                if operation == entry.operation
2820                    && let Some(started) = entry.handler.start(op, doc, view)?
2821                {
2822                    return Ok(Some(started));
2823                }
2824            }
2825            Ok(None)
2826        })
2827    }
2828
2829    pub(crate) fn poll(
2830        &self,
2831        invocation: InvocationId,
2832        cancel: bool,
2833        view: &mut dyn ui_core::widget::Widget,
2834    ) -> Option<InvocationUpdate> {
2835        let causal = self
2836            .inner
2837            .borrow_mut()
2838            .causal
2839            .poll_invocation(invocation, cancel);
2840        causal.or_else(|| {
2841            self.with_handlers(|handlers| {
2842                handlers
2843                    .iter_mut()
2844                    .find_map(|entry| entry.handler.poll(invocation, cancel, view))
2845            })
2846        })
2847    }
2848
2849    /// Finish the jobs the view's windows started
2850    /// ([`ViewCx::window_jobs`](super::ViewCx::window_jobs)).
2851    #[inline(never)]
2852    pub(crate) fn poll_window_jobs(&self, view: &mut dyn ui_core::widget::Widget) {
2853        self.with_handlers(|handlers| {
2854            for entry in handlers {
2855                entry.handler.poll_jobs(view);
2856            }
2857        });
2858    }
2859
2860    /// Run `run` over the live handlers, taken out of the registry so a
2861    /// handler may build view parts that register handlers of their own (a
2862    /// window mounting a child window as an effect or job finishes).
2863    fn with_handlers<T>(&self, run: impl FnOnce(&mut Vec<Registered>) -> T) -> T {
2864        let mut handlers = {
2865            let mut inner = self.inner.borrow_mut();
2866            inner.prune();
2867            std::mem::take(&mut inner.handlers)
2868        };
2869        let ran = run(&mut handlers);
2870        let mut inner = self.inner.borrow_mut();
2871        handlers.append(&mut inner.handlers);
2872        inner.handlers = handlers;
2873        ran
2874    }
2875
2876    pub(crate) fn has_causal_drains(&self) -> bool {
2877        self.inner.borrow().causal.registered()
2878    }
2879
2880    pub(crate) fn start_document_effects(
2881        &self,
2882        operation: &ResourceOperation<'_>,
2883        applied: &ui_core::widget::document_effects::AppliedDocument<'_>,
2884    ) -> Result<Option<ResourceOperationStart>, String> {
2885        self.inner
2886            .borrow_mut()
2887            .causal
2888            .start(operation, applied, read_state::<R>)
2889    }
2890
2891    /// Write the outbox settlements learned since the last settle pass, then
2892    /// tell the invocations waiting on them how they settled.
2893    #[inline(never)]
2894    pub(crate) fn apply_settlements(&self, edits: &mut ui_core::widget::Edits<'_>) {
2895        let settling = Rc::clone(&self.inner.borrow().settling);
2896        apply_settlements(&settling, edits);
2897    }
2898
2899    /// Settle finished projection jobs, then start what the document at
2900    /// `revision` lists and is not running yet.
2901    #[inline(never)]
2902    pub(crate) fn drain_projections(&self, state: &R::State, revision: u64) {
2903        self.inner.borrow_mut().drain_projections(state, revision);
2904    }
2905}
2906
2907/// The resource state `doc` holds, read as `R`'s.
2908fn read_state<R: Resource>(doc: &tilo_resource::Doc) -> Result<Box<dyn Any>, String> {
2909    R::read(doc)
2910        .map(|state| Box::new(state) as Box<dyn Any>)
2911        .map_err(|error| error.to_string())
2912}
2913
2914impl Inner {
2915    fn prune(&mut self) {
2916        self.published
2917            .retain(|entry| entry.alive.strong_count() > 0);
2918        self.handlers.retain(|entry| entry.alive.strong_count() > 0);
2919        self.drains.retain(|entry| entry.alive.strong_count() > 0);
2920    }
2921
2922    fn operations(&mut self) -> Vec<&'static str> {
2923        self.prune();
2924        self.published.iter().map(|entry| entry.operation).collect()
2925    }
2926
2927    fn specs(&mut self) -> Vec<ActionSpec> {
2928        self.prune();
2929        self.published
2930            .iter()
2931            .flat_map(|entry| (entry.specs)())
2932            .collect()
2933    }
2934
2935    fn drain_projections(&mut self, state: &dyn Any, revision: u64) {
2936        if self.drains.is_empty() {
2937            return;
2938        }
2939        self.prune();
2940        let mut settled = false;
2941        for entry in &mut self.drains {
2942            settled |= entry.drain.poll(revision);
2943        }
2944        let outside_changed = self.outside_changed.replace(false);
2945        if !settled && !outside_changed && self.drained == Some(revision) {
2946            return;
2947        }
2948        self.drained = Some(revision);
2949        for entry in &mut self.drains {
2950            if outside_changed {
2951                entry.drain.forget_settled();
2952            }
2953            entry.drain.run(state, revision);
2954        }
2955    }
2956}
2957
2958#[cfg(test)]
2959mod tests;