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}
2214
2215trait 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 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 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#[derive(Clone, Default)]
2272pub struct Projection {
2273 outside_changed: Option<Rc<Cell<bool>>>,
2275}
2276
2277impl Projection {
2278 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
2296trait Drainer {
2298 fn run(&mut self, state: &dyn Any, revision: u64);
2301 fn poll(&mut self, revision: u64) -> bool;
2303 fn forget_settled(&mut self);
2305}
2306
2307type OwnedEntries = Box<dyn Fn(&dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>>>;
2310type StartEntry = Box<
2313 dyn FnMut(
2314 &Id,
2315 Box<dyn Any>,
2316 Option<InvocationId>,
2317 Option<&ResourceInvocationScope>,
2318 ) -> Result<(EffectAnswer, Option<Job>), String>,
2319>;
2320type SettleEntry = Box<
2324 dyn Fn(
2325 &Id,
2326 Result<ActionOutput, String>,
2327 ) -> Result<Option<(ApplySettlement, Result<ActionOutput, ActionFailure>)>, String>,
2328>;
2329
2330struct OutboxDrain {
2335 kind: &'static str,
2336 owned: OwnedEntries,
2337 start: StartEntry,
2338 settle: SettleEntry,
2339 complete: Box<Complete<'static>>,
2341 abandoned: fn() -> Option<serde_json::Value>,
2343 jobs: Running<Id>,
2344 retry: HashMap<Id, u64>,
2347 settled: std::collections::HashSet<Id>,
2350 caused: HashMap<Id, (InvocationId, causal::Group)>,
2353 revision: u64,
2355 settling: Settling,
2357}
2358
2359struct 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
2371fn 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
2388fn 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
2405fn abandoned<O: serde::Serialize + Default>() -> Option<serde_json::Value> {
2407 serde_json::to_value(Settled::<O>::failed("cancelled")).ok()
2408}
2409
2410impl OutboxDrain {
2411 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 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 fn owned(&self, state: &dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>> {
2443 (self.owned)(state)
2444 }
2445
2446 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
2522impl<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 drained: Option<u64>,
2558 outside_changed: Rc<Cell<bool>>,
2561 settling: Settling,
2563}
2564
2565pub(crate) struct Effects<R: Resource> {
2568 env: Option<EffectEnv<R>>,
2569 inner: Rc<RefCell<Inner>>,
2570 jobs: Rc<RefCell<OwnedJobs<R, ()>>>,
2572 starts_work: bool,
2575}
2576
2577#[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 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 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 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 #[inline(never)]
2647 pub(crate) fn view_jobs(&self) -> Jobs<R> {
2648 self.jobs.borrow().jobs.clone()
2649 }
2650
2651 #[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 fn working_env(&self) -> Option<&EffectEnv<R>> {
2664 self.env.as_ref().filter(|_| self.starts_work)
2665 }
2666
2667 #[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 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 pub(crate) fn operations(&self) -> Vec<&'static str> {
2800 self.inner.borrow_mut().operations()
2801 }
2802
2803 #[inline(never)]
2805 pub(crate) fn specs(&self) -> Vec<ActionSpec> {
2806 self.inner.borrow_mut().specs()
2807 }
2808
2809 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 #[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 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 #[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 #[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
2907fn 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;