1use 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#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ChosenFile {
83 pub name: String,
85 pub media_type: Option<String>,
87 pub bytes: Vec<u8>,
88 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
109const CHOOSER_GONE: &str = "the file chooser closed without answering";
111
112#[derive(Clone, Debug, PartialEq)]
114pub enum Effected {
115 Done(ActionOutput),
117 Pending,
119}
120
121impl Effected {
122 pub fn done() -> Self {
124 Self::Done(ActionOutput::default())
125 }
126
127 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 pub fn settled<O: serde::Serialize>(settled: &Settled<O>) -> Self {
140 Self::value(settled)
141 }
142
143 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
156pub type EffectAnswer = Result<Effected, String>;
159
160enum 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
171pub struct Progress {
174 events: Sender<JobEvent>,
175 cancelled: Arc<AtomicBool>,
176 wake: WakeHandle,
177 custody: Option<ResourceEffectScope>,
178}
179
180impl 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 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 pub fn is_cancelled(&self) -> bool {
216 self.with_effect(|| Ok(())).is_err()
217 }
218
219 #[must_use]
224 pub fn cancellation_signal(&self) -> Arc<AtomicBool> {
225 Arc::clone(&self.cancelled)
226 }
227
228 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 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
269pub struct EffectReply {
273 progress: Progress,
274 completion: bool,
275}
276
277impl EffectReply {
278 pub fn progress(&self) -> &Progress {
280 &self.progress
281 }
282
283 pub fn is_cancelled(&self) -> bool {
285 self.progress.is_cancelled()
286 }
287
288 pub fn finish(self, result: Result<ActionOutput, String>) {
291 self.answer(result.map_err(failure));
292 }
293
294 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
320type Source = Box<dyn FnMut() -> Option<Box<dyn Any + Send>>>;
323
324use job::{JobCore, Tracking, track_for};
325
326enum Next {
328 Progress(Polled),
329 Failed(ActionFailure),
331 Finished(Box<dyn Any + Send>),
332}
333
334fn 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
344fn 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
356struct Job {
361 core: JobCore,
362 then: Option<Box<dyn Any>>,
364 owner: std::any::TypeId,
366}
367
368impl Job {
369 fn stop(self) {
370 self.core.stop();
371 }
372}
373
374struct JobCustody {
377 worker: Option<ResourceEffectScope>,
378 custody: Option<ResourceInvocationScope>,
379}
380
381impl JobCustody {
382 #[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#[inline(never)]
395fn owned_scope(custody: Option<&ResourceInvocationScope>) -> Option<ResourceInvocationScope> {
396 custody.cloned()
397}
398
399fn 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
436fn 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
452type 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
463fn 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
491pub(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#[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
536pub struct EffectCx<R: Resource, H = ()> {
542 env: EffectEnv<R>,
543 inv: Answering,
545 host: std::marker::PhantomData<fn(&mut H)>,
547}
548
549struct Answering {
553 invocation: Option<InvocationId>,
554 started: Option<Job>,
555 document: Option<Rc<RefCell<tilo_resource::Doc>>>,
558 heads: Vec<ui_core::automerge::ChangeHash>,
560 custody: Option<ResourceInvocationScope>,
561 worker_custody: Option<ResourceEffectScope>,
562 delivered: Option<InvocationId>,
567 target: Option<ui_core::host_operation::OperationTarget>,
568 place: Option<tilo_resource::Place>,
570 cancel_by: Option<Arc<AtomicBool>>,
574}
575
576impl Answering {
577 #[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
595fn 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#[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#[inline(never)]
617fn place_from(value: serde_json::Value) -> Option<tilo_resource::Place> {
618 serde_json::from_value(value).ok()
620}
621
622pub struct DocumentVersion {
626 pub doc: tilo_resource::Doc,
627 pub heads: Vec<ui_core::automerge::ChangeHash>,
628 pub name: String,
629}
630
631type BoxedFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Box<dyn Any + Send>> + Send>>;
633
634type BoxedWork = Box<dyn FnOnce(Progress) -> Box<dyn Any + Send> + Send>;
636
637fn 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
658fn 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 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 pub fn place(&self) -> Option<&tilo_resource::Place> {
695 self.inv.place.as_ref()
696 }
697
698 pub fn document(&self) -> Option<std::cell::Ref<'_, tilo_resource::Doc>> {
704 self.inv.document.as_ref().map(|document| document.borrow())
705 }
706
707 pub fn heads(&self) -> Vec<ui_core::automerge::ChangeHash> {
709 self.inv.heads.clone()
710 }
711
712 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 pub fn dispatcher(&self) -> Dispatcher<R> {
737 self.env.dispatch.clone()
738 }
739
740 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 pub fn apply(&self, action: &R::Action) {
756 self.env.dispatch.apply(action);
757 }
758
759 pub fn submit(&self, submit: impl super::Submit<R>) {
761 self.env.dispatch.submit(submit);
762 }
763
764 pub fn live(&self) -> &Live<R> {
766 &self.env.live
767 }
768
769 pub fn invocation(&self) -> Option<InvocationId> {
773 self.inv.invocation
774 }
775
776 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 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 tracing::warn!(%error, "these writes stay their person's in file history");
819 None
820 }
821 }
822 }
823
824 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 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 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 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 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 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 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 pub fn cancel_by(&mut self, signal: Arc<AtomicBool>) {
1029 self.inv.cancel_by = Some(signal);
1030 }
1031
1032 pub fn reply(&mut self) -> EffectReply {
1035 let progress = self.track(None);
1036 EffectReply {
1037 progress,
1038 completion: false,
1039 }
1040 }
1041
1042 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1288pub 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#[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#[derive(Default)]
1324struct JobQueue {
1325 started: Vec<(String, Job)>,
1327 cancelled: Vec<String>,
1328 running: std::collections::HashSet<String>,
1329 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 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 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 pub fn is_running(&self, key: &str) -> bool {
1377 self.queue.borrow().running.contains(key)
1378 }
1379
1380 pub fn progress(&self, key: &str) -> Option<JobProgress> {
1383 self.queue.borrow().progress.get(key).cloned()
1384 }
1385}
1386
1387pub(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 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
1420fn 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 #[inline(never)]
1456 fn shared() -> Rc<RefCell<Self>> {
1457 Rc::default()
1458 }
1459
1460 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 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#[derive(Clone, Debug, PartialEq)]
1519pub struct EffectStatus {
1520 id: &'static str,
1521 availability: Option<Availability>,
1522 label: Option<String>,
1523}
1524
1525impl EffectStatus {
1526 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 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 #[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 pub(crate) fn id(&self) -> &'static str {
1561 self.id
1562 }
1563
1564 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
1575pub(crate) type KeptSpecs = std::cell::RefCell<Option<(u64, Vec<EffectStatus>, Vec<ActionSpec>)>>;
1579
1580pub(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#[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#[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
1628fn 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
1639fn 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
1652enum Polled {
1654 Progress {
1655 sequence: u64,
1656 phase: String,
1657 completed: Option<u64>,
1658 total: Option<u64>,
1659 },
1660 Finished(Option<Result<ActionOutput, ActionFailure>>),
1662}
1663
1664pub(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
1681pub(crate) trait JobKey: Eq + Hash + Clone + Sized {
1686 fn running() -> Running<Self>;
1688 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 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 fn cancel(&mut self, key: &K) -> bool {
1744 self.running.remove(key).map(Job::stop).is_some()
1745 }
1746
1747 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 static ACTING_FOR: Cell<Option<InvocationId>> = const { Cell::new(None) };
1785}
1786
1787pub(crate) fn acting_for() -> Option<InvocationId> {
1791 ACTING_FOR.get()
1792}
1793
1794struct 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
1810fn 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
1834fn 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
1848pub(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 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 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
1942fn 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
1953fn 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
1966fn 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
1983struct Answered<'a> {
1985 id: &'a str,
1986 invocation: InvocationId,
1987 job: bool,
1989 answer: EffectAnswer,
1990 started: Option<Job>,
1992}
1993
1994fn 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 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 (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
2058struct InvocationParts {
2061 inv: Answering,
2062 lent: Option<ResourceInvocationScope>,
2064}
2065
2066#[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 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#[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#[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
2169fn poll_invocation(
2173 jobs: &mut Running,
2174 invocation: InvocationId,
2175 cancel: bool,
2176 complete: &mut Complete<'_>,
2177) -> Option<InvocationUpdate> {
2178 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
2208struct Published {
2210 alive: Weak<()>,
2211 operation: &'static str,
2212 specs: Box<dyn Fn() -> Vec<ActionSpec>>,
2213 rows: fn() -> Vec<tilo_resource::effect::EffectRow>,
2216}
2217
2218trait ViewHandler {
2220 fn start(
2221 &mut self,
2222 op: &ResourceOperation<'_>,
2223 doc: &tilo_resource::Doc,
2224 view: &mut dyn ui_core::widget::Widget,
2225 ) -> Result<Option<ResourceOperationStart>, String>;
2226 fn poll(
2227 &mut self,
2228 invocation: InvocationId,
2229 cancel: bool,
2230 view: &mut dyn ui_core::widget::Widget,
2231 ) -> Option<InvocationUpdate>;
2232 fn poll_jobs(&mut self, _view: &mut dyn ui_core::widget::Widget) {}
2234}
2235
2236type Handle<R, E> = Box<dyn FnMut(E, &mut EffectCx<R>) -> EffectAnswer>;
2237
2238struct Handler<R: Resource, E> {
2239 runner: EffectRunner<R, (), E>,
2240 handle: Handle<R, E>,
2241}
2242
2243impl<R: Resource, E: Effect> ViewHandler for Handler<R, E> {
2244 fn start(
2245 &mut self,
2246 op: &ResourceOperation<'_>,
2247 doc: &tilo_resource::Doc,
2248 _view: &mut dyn ui_core::widget::Widget,
2249 ) -> Result<Option<ResourceOperationStart>, String> {
2250 let handle = &mut self.handle;
2251 self.runner
2254 .start(op, &mut (), Some(doc), |(), effect, cx| handle(effect, cx))
2255 }
2256
2257 fn poll(
2258 &mut self,
2259 invocation: InvocationId,
2260 cancel: bool,
2261 _view: &mut dyn ui_core::widget::Widget,
2262 ) -> Option<InvocationUpdate> {
2263 self.runner.poll(invocation, cancel, &mut ())
2264 }
2265}
2266
2267#[derive(Clone, Default)]
2275pub struct Projection {
2276 outside_changed: Option<Rc<Cell<bool>>>,
2278}
2279
2280impl Projection {
2281 pub fn outside_changed(&self) {
2285 if let Some(flag) = &self.outside_changed {
2286 flag.set(true);
2287 }
2288 }
2289}
2290
2291impl std::fmt::Debug for Projection {
2292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2293 f.debug_struct("Projection")
2294 .field("runs", &self.outside_changed.is_some())
2295 .finish()
2296 }
2297}
2298
2299trait Drainer {
2301 fn run(&mut self, state: &dyn Any, revision: u64);
2304 fn poll(&mut self, revision: u64) -> bool;
2306 fn forget_settled(&mut self);
2308}
2309
2310type OwnedEntries = Box<dyn Fn(&dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>>>;
2313type StartEntry = Box<
2316 dyn FnMut(
2317 &Id,
2318 Box<dyn Any>,
2319 Option<InvocationId>,
2320 Option<&ResourceInvocationScope>,
2321 ) -> Result<(EffectAnswer, Option<Job>), String>,
2322>;
2323type SettleEntry = Box<
2327 dyn Fn(
2328 &Id,
2329 Result<ActionOutput, String>,
2330 ) -> Result<Option<(ApplySettlement, Result<ActionOutput, ActionFailure>)>, String>,
2331>;
2332
2333struct OutboxDrain {
2338 kind: &'static str,
2339 owned: OwnedEntries,
2340 start: StartEntry,
2341 settle: SettleEntry,
2342 complete: Box<Complete<'static>>,
2344 abandoned: fn() -> Option<serde_json::Value>,
2346 jobs: Running<Id>,
2347 retry: HashMap<Id, u64>,
2350 settled: std::collections::HashSet<Id>,
2353 caused: HashMap<Id, (InvocationId, causal::Group)>,
2356 revision: u64,
2358 settling: Settling,
2360}
2361
2362struct Settlement {
2366 apply: ApplySettlement,
2367 waiting: Option<(causal::Group, Result<ActionOutput, ActionFailure>)>,
2368}
2369
2370type Settling = Rc<RefCell<Vec<Settlement>>>;
2371type ApplySettlement =
2372 Box<dyn FnOnce(&mut ui_core::widget::Edits<'_>) -> Result<(), tilo_actions::ActionError>>;
2373
2374fn apply_settlements(settling: &Settling, edits: &mut ui_core::widget::Edits<'_>) {
2377 let settling = std::mem::take(&mut *settling.borrow_mut());
2378 for Settlement { apply, waiting } in settling {
2379 let applied = apply(edits);
2380 if let Some((group, told)) = waiting {
2381 group.borrow_mut().finished(match applied {
2382 Ok(()) => told,
2383 Err(error) => Err(failure(error.to_string())),
2384 });
2385 } else if let Err(error) = applied {
2386 tracing::warn!(%error, "an outbox settlement was refused");
2387 }
2388 }
2389}
2390
2391fn settlement<O>(output: &ActionOutput) -> Result<Settled<O>, String>
2395where
2396 O: serde::de::DeserializeOwned + Default,
2397{
2398 let Some(value) = &output.value else {
2399 return Ok(Settled::Done(O::default()));
2400 };
2401 serde_json::from_value::<Settled<O>>(value.clone())
2402 .or_else(|_| serde_json::from_value::<O>(value.clone()).map(Settled::Done))
2403 .map_err(|error| {
2404 format!("an outbox handler answered what its entry does not settle with: {error}")
2405 })
2406}
2407
2408fn abandoned<O: serde::Serialize + Default>() -> Option<serde_json::Value> {
2410 serde_json::to_value(Settled::<O>::failed("cancelled")).ok()
2411}
2412
2413impl OutboxDrain {
2414 fn finish(&mut self, id: &Id, answer: Result<ActionOutput, String>) -> bool {
2417 let group = self.caused.remove(id).map(|(_, group)| group);
2418 match (self.settle)(id, answer) {
2419 Ok(Some((apply, told))) => {
2420 self.settled.insert(id.clone());
2421 self.settling.borrow_mut().push(Settlement {
2424 apply,
2425 waiting: group.map(|group| (group, told)),
2426 });
2427 true
2428 }
2429 Ok(None) => {
2430 self.settled.insert(id.clone());
2431 false
2432 }
2433 Err(error) => {
2434 tracing::warn!(%error, kind = self.kind, "an outbox entry stays pending");
2435 if let Some(group) = group {
2436 group.borrow_mut().finished(Err(failure(error)));
2437 }
2438 self.retry.insert(id.clone(), self.revision);
2439 false
2440 }
2441 }
2442 }
2443
2444 fn owned(&self, state: &dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>> {
2446 (self.owned)(state)
2447 }
2448
2449 fn start_entry(
2452 &mut self,
2453 id: Id,
2454 value: Box<dyn Any>,
2455 invocation: Option<InvocationId>,
2456 custody: Option<&ResourceInvocationScope>,
2457 ) -> Result<(), String> {
2458 let (answer, started) = (self.start)(&id, value, invocation, custody)?;
2459 match (answer, started) {
2460 (Ok(Effected::Pending), Some(started)) => self.jobs.insert(id, started),
2461 (answer, started) => {
2462 if let Some(started) = started {
2463 started.stop();
2464 }
2465 let answer = answer.and_then(|effected| match effected {
2466 Effected::Done(output) => Ok(output),
2467 Effected::Pending => {
2468 Err("an outbox handler answered later without starting a job".into())
2469 }
2470 });
2471 self.finish(&id, answer);
2472 }
2473 }
2474 Ok(())
2475 }
2476}
2477
2478impl Drainer for OutboxDrain {
2479 fn run(&mut self, state: &dyn Any, revision: u64) {
2480 let Some(pending) = self.owned(state) else {
2481 return;
2482 };
2483 self.revision = revision;
2484 self.settled
2485 .retain(|id| pending.iter().any(|(entry, _)| entry == id));
2486 self.retry
2487 .retain(|id, _| pending.iter().any(|(entry, _)| entry == id));
2488 for (id, value) in pending {
2489 if self.jobs.contains(&id)
2490 || self.settled.contains(&id)
2491 || self.retry.get(&id) == Some(&revision)
2492 {
2493 continue;
2494 }
2495 if let Err(error) = self.start_entry(id, value, None, None) {
2496 tracing::warn!(%error, kind = self.kind, "an outbox entry did not start");
2497 }
2498 }
2499 }
2500
2501 fn poll(&mut self, revision: u64) -> bool {
2502 self.revision = revision;
2503 let mut finished = false;
2504 for id in self.jobs.keys() {
2505 let invocation = self.caused.get(&id).map(|(invocation, _)| *invocation);
2506 while let Some(polled) = self.jobs.poll(&id, &mut *self.complete, invocation) {
2507 let Polled::Finished(result) = polled else {
2508 continue;
2509 };
2510 if let Some(result) = result {
2511 self.finish(&id, result.map_err(|error| error.message));
2512 finished = true;
2513 }
2514 break;
2515 }
2516 }
2517 finished
2518 }
2519
2520 fn forget_settled(&mut self) {
2521 self.retry.clear();
2522 }
2523}
2524
2525impl<D: Drainer> Drainer for Rc<RefCell<D>> {
2529 fn run(&mut self, state: &dyn Any, revision: u64) {
2530 self.borrow_mut().run(state, revision);
2531 }
2532
2533 fn poll(&mut self, revision: u64) -> bool {
2534 self.borrow_mut().poll(revision)
2535 }
2536
2537 fn forget_settled(&mut self) {
2538 self.borrow_mut().forget_settled();
2539 }
2540}
2541
2542struct Registered {
2543 alive: Weak<()>,
2544 operation: &'static str,
2545 handler: Box<dyn ViewHandler>,
2546}
2547
2548struct Drains {
2549 alive: Weak<()>,
2550 drain: Box<dyn Drainer>,
2551}
2552
2553#[derive(Default)]
2554struct Inner {
2555 causal: causal::CausalEffects,
2556 published: Vec<Published>,
2557 handlers: Vec<Registered>,
2558 drains: Vec<Drains>,
2559 drained: Option<u64>,
2561 outside_changed: Rc<Cell<bool>>,
2564 settling: Settling,
2566}
2567
2568pub(crate) struct Effects<R: Resource> {
2571 env: Option<EffectEnv<R>>,
2572 inner: Rc<RefCell<Inner>>,
2573 jobs: Rc<RefCell<OwnedJobs<R, ()>>>,
2575 starts_work: bool,
2578}
2579
2580#[inline(never)]
2583fn new_inner() -> Rc<RefCell<Inner>> {
2584 Rc::default()
2585}
2586
2587impl<R: Resource> Clone for Effects<R> {
2588 fn clone(&self) -> Self {
2589 Self {
2590 env: self.env.clone(),
2591 inner: Rc::clone(&self.inner),
2592 jobs: Rc::clone(&self.jobs),
2593 starts_work: self.starts_work,
2594 }
2595 }
2596}
2597
2598impl<R: Resource> Effects<R> {
2599 pub(crate) fn live(dispatch: Dispatcher<R>, live: Live<R>) -> Self {
2601 let env = EffectEnv {
2602 dispatch,
2603 live,
2604 wake: WakeHandle::new(),
2605 scope: None,
2606 };
2607 Self {
2608 jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(Some(env.clone()))))),
2609 env: Some(env),
2610 inner: new_inner(),
2611 starts_work: true,
2612 }
2613 }
2614
2615 pub(crate) fn invoked(dispatch: Dispatcher<R>, live: Live<R>) -> Self {
2620 Self {
2621 env: Some(EffectEnv {
2622 dispatch,
2623 live,
2624 wake: WakeHandle::new(),
2625 scope: None,
2626 }),
2627 inner: new_inner(),
2628 jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(None)))),
2629 starts_work: false,
2630 }
2631 }
2632
2633 pub(crate) fn inert() -> Self {
2636 Self {
2637 env: None,
2638 inner: new_inner(),
2639 jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(None)))),
2640 starts_work: false,
2641 }
2642 }
2643
2644 pub(crate) fn env(&self) -> Option<&EffectEnv<R>> {
2645 self.env.as_ref()
2646 }
2647
2648 #[inline(never)]
2650 pub(crate) fn view_jobs(&self) -> Jobs<R> {
2651 self.jobs.borrow().jobs.clone()
2652 }
2653
2654 #[inline(never)]
2656 pub(crate) fn surface_jobs(&self) -> Jobs<R> {
2657 Jobs::new(self.working_env().cloned())
2658 }
2659
2660 pub(crate) fn scoped_surface_jobs(&self, scope: &Rc<super::surface::SurfaceScope>) -> Jobs<R> {
2661 Jobs::new(self.working_env().map(|env| env.scoped(scope)))
2662 }
2663
2664 fn working_env(&self) -> Option<&EffectEnv<R>> {
2667 self.env.as_ref().filter(|_| self.starts_work)
2668 }
2669
2670 #[inline(never)]
2672 pub(crate) fn poll_jobs(&self) {
2673 self.jobs.borrow_mut().poll(&mut ());
2674 self.inner.borrow_mut().causal.poll();
2675 }
2676
2677 pub(crate) fn publish<E: Effect>(
2680 &self,
2681 alive: Weak<()>,
2682 specs: impl Fn() -> Vec<ActionSpec> + 'static,
2683 ) {
2684 self.inner.borrow_mut().published.push(Published {
2685 alive,
2686 operation: E::OPERATION,
2687 specs: Box::new(specs),
2688 rows: E::rows,
2689 });
2690 }
2691
2692 pub(crate) fn handle<E: Effect>(
2693 &self,
2694 alive: Weak<()>,
2695 handle: impl FnMut(E, &mut EffectCx<R>) -> EffectAnswer + 'static,
2696 ) {
2697 let Some(env) = self.env.clone() else {
2698 return;
2699 };
2700 self.inner.borrow_mut().handlers.push(Registered {
2701 alive,
2702 operation: E::OPERATION,
2703 handler: Box::new(Handler {
2704 runner: EffectRunner::<R, (), E>::new(env),
2705 handle: Box::new(handle),
2706 }),
2707 });
2708 }
2709
2710 pub(crate) fn outbox<T, O>(
2711 &self,
2712 alive: Weak<()>,
2713 outbox: Path<R, Outbox<T, O>>,
2714 owns: impl Fn(&R::State, &T) -> bool + 'static,
2715 run: impl FnMut(&Id, T, &mut EffectCx<R>) -> EffectAnswer + 'static,
2716 awaited: bool,
2717 ) -> Projection
2718 where
2719 R: Resource<State = R> + tilo_resource::Model + 'static,
2720 T: tilo_resource::Model + Clone + 'static,
2721 O: serde::Serialize + serde::de::DeserializeOwned + Default + 'static,
2722 {
2723 let Some(env) = self.working_env().cloned() else {
2724 return Projection::default();
2725 };
2726 let reader = outbox.clone();
2727 let owned: OwnedEntries = Box::new(move |state: &dyn Any| {
2728 let state = state.downcast_ref::<R>()?;
2729 Some(reader.get(state).map_or_else(Vec::new, |outbox| {
2730 outbox
2731 .pending()
2732 .filter(|(_, value)| owns(state, value))
2733 .map(|(id, value)| (id.clone(), Box::new(value.clone()) as Box<dyn Any>))
2734 .collect()
2735 }))
2736 });
2737 let mut run = run;
2738 let starting = env.clone();
2739 let start: StartEntry = Box::new(move |id, value, invocation, custody| {
2740 let Ok(value) = value.downcast::<T>() else {
2741 return Err("an outbox entry is of another type".into());
2742 };
2743 let mut cx = EffectCx::invoked(
2744 starting.clone(),
2745 custody_parts(invocation, owned_scope(custody))?,
2746 );
2747 let mut value = Some(*value);
2748 let answer = run_under(invocation, custody, &mut || match value.take() {
2749 Some(value) => run(id, value, &mut cx),
2750 None => Err("an outbox entry ran twice".into()),
2751 });
2752 Ok((answer, cx.inv.started.take()))
2753 });
2754 let dispatch = env.dispatch.clone();
2755 let settle: SettleEntry = Box::new(move |id, answer| {
2756 let settled = answer.and_then(|output| settlement::<O>(&output))?;
2757 let told = match &settled {
2758 Settled::Done(_) => Ok(ActionOutput::default()),
2759 Settled::Failed { reason, .. } => Err(failure(reason.clone())),
2760 };
2761 let Some(action) = R::edit_action(outbox.settle(id, &settled)) else {
2762 return Ok(None);
2763 };
2764 let dispatch = dispatch.clone();
2765 let apply: ApplySettlement = Box::new(move |edits| dispatch.apply_now(edits, &action));
2766 Ok(Some((apply, told)))
2767 });
2768 let completing = env;
2769 let complete: Box<Complete<'static>> = Box::new(move |then, value, custody, invocation| {
2770 complete::<R, ()>(&completing, &mut (), then, value, custody, invocation)
2771 });
2772 let drain = Rc::new(RefCell::new(OutboxDrain {
2773 kind: R::KIND,
2774 owned,
2775 start,
2776 settle,
2777 complete,
2778 abandoned: abandoned::<O>,
2779 jobs: Running::default(),
2780 retry: HashMap::new(),
2781 settled: std::collections::HashSet::new(),
2782 caused: HashMap::new(),
2783 revision: 0,
2784 settling: Rc::clone(&self.inner.borrow().settling),
2785 }));
2786 let mut inner = self.inner.borrow_mut();
2787 if awaited {
2788 inner
2789 .causal
2790 .register_drainer(alive.clone(), Box::new(Rc::clone(&drain)));
2791 }
2792 inner.drains.push(Drains {
2793 alive,
2794 drain: Box::new(drain),
2795 });
2796 inner.drained = None;
2797 Projection {
2798 outside_changed: Some(Rc::clone(&inner.outside_changed)),
2799 }
2800 }
2801
2802 pub(crate) fn operations(&self) -> Vec<&'static str> {
2804 self.inner.borrow_mut().operations()
2805 }
2806
2807 #[inline(never)]
2809 pub(crate) fn specs(&self) -> Vec<ActionSpec> {
2810 self.inner.borrow_mut().specs()
2811 }
2812
2813 #[inline(never)]
2816 pub(crate) fn rows(&self) -> Vec<tilo_resource::effect::EffectRow> {
2817 self.inner.borrow_mut().rows()
2818 }
2819
2820 pub(crate) fn start(
2822 &self,
2823 op: &ResourceOperation<'_>,
2824 doc: &tilo_resource::Doc,
2825 view: &mut dyn ui_core::widget::Widget,
2826 ) -> Result<Option<ResourceOperationStart>, String> {
2827 let operation = op.with_custody().0.type_id;
2828 self.with_handlers(|handlers| {
2829 for entry in handlers {
2830 if operation == entry.operation
2831 && let Some(started) = entry.handler.start(op, doc, view)?
2832 {
2833 return Ok(Some(started));
2834 }
2835 }
2836 Ok(None)
2837 })
2838 }
2839
2840 pub(crate) fn poll(
2841 &self,
2842 invocation: InvocationId,
2843 cancel: bool,
2844 view: &mut dyn ui_core::widget::Widget,
2845 ) -> Option<InvocationUpdate> {
2846 let causal = self
2847 .inner
2848 .borrow_mut()
2849 .causal
2850 .poll_invocation(invocation, cancel);
2851 causal.or_else(|| {
2852 self.with_handlers(|handlers| {
2853 handlers
2854 .iter_mut()
2855 .find_map(|entry| entry.handler.poll(invocation, cancel, view))
2856 })
2857 })
2858 }
2859
2860 #[inline(never)]
2863 pub(crate) fn poll_window_jobs(&self, view: &mut dyn ui_core::widget::Widget) {
2864 self.with_handlers(|handlers| {
2865 for entry in handlers {
2866 entry.handler.poll_jobs(view);
2867 }
2868 });
2869 }
2870
2871 fn with_handlers<T>(&self, run: impl FnOnce(&mut Vec<Registered>) -> T) -> T {
2875 let mut handlers = {
2876 let mut inner = self.inner.borrow_mut();
2877 inner.prune();
2878 std::mem::take(&mut inner.handlers)
2879 };
2880 let ran = run(&mut handlers);
2881 let mut inner = self.inner.borrow_mut();
2882 handlers.append(&mut inner.handlers);
2883 inner.handlers = handlers;
2884 ran
2885 }
2886
2887 pub(crate) fn has_causal_drains(&self) -> bool {
2888 self.inner.borrow().causal.registered()
2889 }
2890
2891 pub(crate) fn start_document_effects(
2892 &self,
2893 operation: &ResourceOperation<'_>,
2894 applied: &ui_core::widget::document_effects::AppliedDocument<'_>,
2895 ) -> Result<Option<ResourceOperationStart>, String> {
2896 self.inner
2897 .borrow_mut()
2898 .causal
2899 .start(operation, applied, read_state::<R>)
2900 }
2901
2902 #[inline(never)]
2905 pub(crate) fn apply_settlements(&self, edits: &mut ui_core::widget::Edits<'_>) {
2906 let settling = Rc::clone(&self.inner.borrow().settling);
2907 apply_settlements(&settling, edits);
2908 }
2909
2910 #[inline(never)]
2913 pub(crate) fn drain_projections(&self, state: &R::State, revision: u64) {
2914 self.inner.borrow_mut().drain_projections(state, revision);
2915 }
2916}
2917
2918fn read_state<R: Resource>(doc: &tilo_resource::Doc) -> Result<Box<dyn Any>, String> {
2920 R::read(doc)
2921 .map(|state| Box::new(state) as Box<dyn Any>)
2922 .map_err(|error| error.to_string())
2923}
2924
2925impl Inner {
2926 fn prune(&mut self) {
2927 self.published
2928 .retain(|entry| entry.alive.strong_count() > 0);
2929 self.handlers.retain(|entry| entry.alive.strong_count() > 0);
2930 self.drains.retain(|entry| entry.alive.strong_count() > 0);
2931 }
2932
2933 fn operations(&mut self) -> Vec<&'static str> {
2934 self.prune();
2935 self.published.iter().map(|entry| entry.operation).collect()
2936 }
2937
2938 fn specs(&mut self) -> Vec<ActionSpec> {
2939 self.prune();
2940 self.published
2941 .iter()
2942 .flat_map(|entry| (entry.specs)())
2943 .collect()
2944 }
2945
2946 fn rows(&mut self) -> Vec<tilo_resource::effect::EffectRow> {
2947 self.prune();
2948 self.published
2949 .iter()
2950 .flat_map(|entry| (entry.rows)())
2951 .collect()
2952 }
2953
2954 fn drain_projections(&mut self, state: &dyn Any, revision: u64) {
2955 if self.drains.is_empty() {
2956 return;
2957 }
2958 self.prune();
2959 let mut settled = false;
2960 for entry in &mut self.drains {
2961 settled |= entry.drain.poll(revision);
2962 }
2963 let outside_changed = self.outside_changed.replace(false);
2964 if !settled && !outside_changed && self.drained == Some(revision) {
2965 return;
2966 }
2967 self.drained = Some(revision);
2968 for entry in &mut self.drains {
2969 if outside_changed {
2970 entry.drain.forget_settled();
2971 }
2972 entry.drain.run(state, revision);
2973 }
2974 }
2975}
2976
2977#[cfg(test)]
2978mod tests;