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;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
33use ui_core::automerge::transaction::Transactable;
34
35use tilo_actions::{
36 ActionFailure, ActionOutput, ActionSpec, Availability, InvocationId, InvocationUpdate,
37};
38use tilo_resource::{ActionRef, Effect, Id, Outbox, Path, Resource, Settled};
39use ui_core::WakeHandle;
40use ui_core::host_operation::{ResourceOperation, ResourceOperationStart};
41use ui_core::invocation_scope::{ResourceEffectScope, ResourceInvocationScope};
42use ui_core::portal::{ContentsOutcome, PickRequest, PickedContents, PortalOutcome, SaveOutcome};
43
44use super::dispatch::Dispatcher;
45use super::live::Live;
46
47fn attach_approved_check_reporter(
48 checked: ui_core::invocation_scope::ApprovedCheckReportedWork,
49 progress: Progress,
50) -> ui_core::invocation_scope::ApprovedCheckFuture {
51 checked(Box::new(move |observation| {
52 progress.with_effect(|| {
53 match observation {
54 ui_core::invocation_scope::ApprovedCheckReportObservation::Reported {
55 phase,
56 completed,
57 total,
58 } => {
59 progress.report(phase, completed, total);
60 }
61 ui_core::invocation_scope::ApprovedCheckReportObservation::Disabled { .. } => {
62 progress.report(
63 ui_core::i18n::I18nManager::lookup_plain("approved-check-reports-disabled"),
64 None,
65 None,
66 );
67 }
68 }
69 Ok(())
70 })
71 }))
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct ChosenFile {
78 pub name: String,
80 pub media_type: Option<String>,
82 pub bytes: Vec<u8>,
83 pub identity: Option<ui_core::record::RecordId>,
88}
89
90impl From<PickedContents> for ChosenFile {
91 fn from(picked: PickedContents) -> Self {
92 Self {
93 identity: picked
94 .location
95 .as_deref()
96 .map(ui_core::record::RecordId::identify_file),
97 name: picked.name,
98 media_type: picked.media_type,
99 bytes: picked.bytes,
100 }
101 }
102}
103
104const CHOOSER_GONE: &str = "the file chooser closed without answering";
106
107#[derive(Clone, Debug, PartialEq)]
109pub enum Effected {
110 Done(ActionOutput),
112 Pending,
114}
115
116impl Effected {
117 pub fn done() -> Self {
119 Self::Done(ActionOutput::default())
120 }
121
122 pub fn value(value: impl serde::Serialize) -> Self {
125 Self::Done(ActionOutput {
126 value: Some(serde_json::to_value(value).unwrap_or(serde_json::Value::Null)),
127 resources: Vec::new(),
128 })
129 }
130
131 pub fn settled<O: serde::Serialize>(settled: &Settled<O>) -> Self {
135 Self::value(settled)
136 }
137
138 pub fn content(hash: &str) -> Result<Self, String> {
145 let resource = tilo_actions::content_resource(hash)
146 .ok_or_else(|| format!("{hash:?} is not a content hash"))?;
147 Ok(Self::Done(ActionOutput {
148 value: None,
149 resources: vec![resource],
150 }))
151 }
152}
153
154pub type EffectAnswer = Result<Effected, String>;
157
158enum JobEvent {
160 Progress {
161 phase: String,
162 completed: Option<u64>,
163 total: Option<u64>,
164 },
165 Finished(Box<dyn Any + Send>),
166 Failed(ActionFailure),
167}
168
169#[derive(Clone)]
172pub struct Progress {
173 events: Sender<JobEvent>,
174 cancelled: Arc<AtomicBool>,
175 wake: WakeHandle,
176 custody: Option<ResourceEffectScope>,
177}
178
179impl Progress {
180 pub fn report(&self, phase: impl Into<String>, completed: Option<u64>, total: Option<u64>) {
182 if self
183 .events
184 .send(JobEvent::Progress {
185 phase: phase.into(),
186 completed,
187 total,
188 })
189 .is_ok()
190 {
191 self.wake.wake();
192 }
193 }
194
195 pub fn is_cancelled(&self) -> bool {
201 self.with_effect(|| Ok(())).is_err()
202 }
203
204 pub fn with_effect<T>(&self, effect: impl FnOnce() -> Result<T, String>) -> Result<T, String> {
214 if self.cancelled.load(Ordering::Acquire) {
215 return Err("the effect was cancelled".into());
216 }
217 let admitted = || {
218 if self.cancelled.load(Ordering::Acquire) {
221 return Err("the effect was cancelled".into());
222 }
223 effect()
224 };
225 match &self.custody {
226 Some(custody) => custody.with_effect(admitted),
227 None => admitted(),
228 }
229 }
230
231 fn fail(&self, error: String) {
232 self.cancelled.store(true, Ordering::Release);
233 if self.events.send(JobEvent::Failed(failure(error))).is_ok() {
234 self.wake.wake();
235 }
236 }
237
238 fn finish(&self, value: Box<dyn Any + Send>) {
239 if self.events.send(JobEvent::Finished(value)).is_ok() {
240 self.wake.wake();
241 }
242 }
243}
244
245pub struct EffectReply {
249 progress: Progress,
250 completion: bool,
251}
252
253impl EffectReply {
254 pub fn progress(&self) -> &Progress {
256 &self.progress
257 }
258
259 pub fn is_cancelled(&self) -> bool {
261 self.progress.is_cancelled()
262 }
263
264 pub fn finish(self, result: Result<ActionOutput, String>) {
267 self.answer(result.map_err(failure));
268 }
269
270 pub fn answer(self, result: Result<ActionOutput, ActionFailure>) {
273 if self.completion {
274 match result {
275 Ok(output) => self.progress.finish(Box::new(output)),
276 Err(error) => {
277 if self.progress.events.send(JobEvent::Failed(error)).is_ok() {
278 self.progress.wake.wake();
279 }
280 }
281 }
282 } else {
283 self.progress.finish(Box::new(result));
284 }
285 }
286}
287
288impl std::fmt::Debug for EffectReply {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 f.debug_struct("EffectReply").finish_non_exhaustive()
291 }
292}
293
294type Then<R, H> = Box<dyn FnOnce(&mut H, Box<dyn Any + Send>, &mut EffectCx<R, H>) -> EffectAnswer>;
295
296type Source = Box<dyn FnMut() -> Option<Box<dyn Any + Send>>>;
299
300struct JobCore {
303 events: Receiver<JobEvent>,
304 cancelled: Arc<AtomicBool>,
305 task: Option<ui_core::rt::JoinHandle<()>>,
306 source: Option<Source>,
307 sequence: u64,
308 custody: Option<ResourceInvocationScope>,
309}
310
311impl JobCore {
312 fn stop(self) {
313 self.cancelled.store(true, Ordering::Release);
314 if let Some(task) = self.task {
315 task.abort();
316 }
317 }
318
319 fn next(&mut self) -> Option<Next> {
321 if let Some(error) = self
322 .custody
323 .as_ref()
324 .and_then(|custody| custody.check().err())
325 {
326 return Some(Next::Failed(failure(error)));
327 }
328 let answered = self.source.as_mut().and_then(|source| source());
329 let event = match answered.map_or_else(
330 || self.events.try_recv(),
331 |value| Ok(JobEvent::Finished(value)),
332 ) {
333 Ok(event) => event,
334 Err(TryRecvError::Empty) => return None,
335 Err(TryRecvError::Disconnected) => {
336 return Some(Next::Failed(failure(
337 "the effect ended without an answer".into(),
338 )));
339 }
340 };
341 if let Some(error) = self
342 .custody
343 .as_ref()
344 .and_then(|custody| custody.check().err())
345 {
346 return Some(Next::Failed(failure(error)));
347 }
348 Some(match event {
349 JobEvent::Failed(error) => Next::Failed(error),
350 JobEvent::Progress {
351 phase,
352 completed,
353 total,
354 } => {
355 self.sequence = self.sequence.saturating_add(1);
356 Next::Progress(Polled::Progress {
357 sequence: self.sequence,
358 phase,
359 completed,
360 total,
361 })
362 }
363 JobEvent::Finished(value) => Next::Finished(value),
364 })
365 }
366}
367
368enum Next {
370 Progress(Polled),
371 Failed(ActionFailure),
373 Finished(Box<dyn Any + Send>),
374}
375
376fn answered(value: Box<dyn Any + Send>) -> Polled {
378 Polled::Finished(Some(
379 match value.downcast::<Result<ActionOutput, ActionFailure>>() {
380 Ok(result) => *result,
381 Err(_) => Err(failure("the effect answered with another type".into())),
382 },
383 ))
384}
385
386fn completed(answer: EffectAnswer, chained: bool) -> Option<Result<ActionOutput, ActionFailure>> {
388 match answer {
389 Ok(Effected::Pending) if chained => None,
390 Ok(Effected::Pending) => Some(Err(failure(
391 "the effect's completion answered later but started no job".into(),
392 ))),
393 Ok(Effected::Done(output)) => Some(Ok(output)),
394 Err(error) => Some(Err(failure(error))),
395 }
396}
397
398struct Job {
403 core: JobCore,
404 then: Option<Box<dyn Any>>,
406 owner: std::any::TypeId,
408}
409
410impl Job {
411 fn stop(self) {
412 self.core.stop();
413 }
414}
415
416struct JobCustody {
419 worker: Option<ResourceEffectScope>,
420 custody: Option<ResourceInvocationScope>,
421}
422
423impl JobCustody {
424 #[inline(never)]
427 fn of(worker: Option<&ResourceEffectScope>, custody: Option<&ResourceInvocationScope>) -> Self {
428 Self {
429 worker: worker.cloned(),
430 custody: custody.cloned(),
431 }
432 }
433}
434
435#[inline(never)]
437fn owned_scope(custody: Option<&ResourceInvocationScope>) -> Option<ResourceInvocationScope> {
438 custody.cloned()
439}
440
441fn track_job(
445 started: &mut Option<Job>,
446 wake: &WakeHandle,
447 custody: JobCustody,
448 then: Option<Box<dyn Any>>,
449 owner: std::any::TypeId,
450) -> Progress {
451 let (events, receiver) = channel();
452 let cancelled = Arc::new(AtomicBool::new(false));
453 let progress = Progress {
454 events,
455 cancelled: Arc::clone(&cancelled),
456 wake: wake.clone(),
457 custody: custody.worker,
458 };
459 if let Some(previous) = started.replace(Job {
460 core: JobCore {
461 events: receiver,
462 cancelled,
463 task: None,
464 source: None,
465 sequence: 0,
466 custody: custody.custody,
467 },
468 then,
469 owner,
470 }) {
471 tracing::warn!("an effect started a second job; the first one is stopped");
472 previous.stop();
473 }
474 progress
475}
476
477fn run_under(
480 invocation: Option<InvocationId>,
481 custody: Option<&ResourceInvocationScope>,
482 run: &mut dyn FnMut() -> EffectAnswer,
483) -> EffectAnswer {
484 let _acting = ActingFor::enter(invocation);
485 match custody {
486 Some(custody) => custody
487 .with_effect(run)
488 .and_then(|answer| custody.check().map(|()| answer)),
489 None => run(),
490 }
491}
492
493type Complete<'a> = dyn FnMut(
497 Box<dyn Any>,
498 Box<dyn Any + Send>,
499 Option<ResourceInvocationScope>,
500 Option<InvocationId>,
501 ) -> Result<(EffectAnswer, Option<Job>), String>
502 + 'a;
503
504fn complete<R: Resource, H: 'static>(
507 env: &EffectEnv<R>,
508 host: &mut H,
509 then: Box<dyn Any>,
510 value: Box<dyn Any + Send>,
511 custody: Option<ResourceInvocationScope>,
512 invocation: Option<InvocationId>,
513) -> Result<(EffectAnswer, Option<Job>), String> {
514 let Ok(then) = then.downcast::<Then<R, H>>() else {
515 return Err("a job's completion is of another type".into());
516 };
517 let scope = owned_scope(custody.as_ref());
518 let mut cx = EffectCx::<R, H>::new(env.clone(), invocation).with_custody(custody)?;
519 let mut then = Some(*then);
520 let mut value = Some(value);
521 let answer = run_under(
522 invocation,
523 scope.as_ref(),
524 &mut || match (then.take(), value.take()) {
525 (Some(then), Some(value)) => then(host, value, &mut cx),
526 _ => Err("a job's completion ran twice".into()),
527 },
528 );
529 Ok((answer, cx.started.take()))
530}
531
532pub(crate) struct EffectEnv<R: Resource> {
534 pub(crate) dispatch: Dispatcher<R>,
535 pub(crate) live: Live<R>,
536 pub(crate) wake: WakeHandle,
537}
538
539impl<R: Resource> Clone for EffectEnv<R> {
540 fn clone(&self) -> Self {
541 Self {
542 dispatch: self.dispatch.clone(),
543 live: self.live.clone(),
544 wake: self.wake.clone(),
545 }
546 }
547}
548
549pub struct EffectCx<R: Resource, H = ()> {
555 env: EffectEnv<R>,
556 invocation: Option<InvocationId>,
557 started: Option<Job>,
558 document: Option<Rc<RefCell<tilo_resource::Doc>>>,
561 heads: Vec<ui_core::automerge::ChangeHash>,
563 custody: Option<ResourceInvocationScope>,
564 worker_custody: Option<ResourceEffectScope>,
565 target: Option<ui_core::host_operation::OperationTarget>,
566 place: Option<tilo_resource::Place>,
568 host: std::marker::PhantomData<fn(&mut H)>,
570}
571
572fn place_of<E: Effect>(
575 decl: &tilo_resource::effect::EffectDecl,
576 effect: &E,
577) -> Option<tilo_resource::Place> {
578 let param = decl.params.iter().find(|param| param.is_place())?;
579 let value = effect.arguments().remove(param.name.as_ref())?;
580 serde_json::from_value(value).ok()
581}
582
583pub struct DocumentVersion {
587 pub doc: tilo_resource::Doc,
588 pub heads: Vec<ui_core::automerge::ChangeHash>,
589 pub name: String,
590}
591
592type BoxedFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Box<dyn Any + Send>> + Send>>;
594
595type BoxedWork = Box<dyn FnOnce(Progress) -> Box<dyn Any + Send> + Send>;
597
598fn spawn_future(progress: Progress, future: BoxedFuture) -> ui_core::rt::JoinHandle<()> {
602 ui_core::rt::app().spawn(async move {
603 let mut future = future;
604 let value = std::future::poll_fn(|cx| {
605 match progress.with_effect(|| Ok(future.as_mut().poll(cx))) {
606 Ok(std::task::Poll::Ready(value)) => std::task::Poll::Ready(Ok(value)),
607 Ok(std::task::Poll::Pending) => std::task::Poll::Pending,
608 Err(error) => std::task::Poll::Ready(Err(error)),
609 }
610 })
611 .await;
612 match value {
613 Ok(value) => progress.finish(value),
614 Err(error) => progress.fail(error),
615 }
616 })
617}
618
619fn spawn_work(progress: Progress, work: BoxedWork) -> ui_core::rt::JoinHandle<()> {
622 ui_core::rt::app().spawn_blocking(move || {
623 match progress.with_effect(|| Ok(work(progress.clone()))) {
624 Ok(value) => progress.finish(value),
625 Err(error) => progress.fail(error),
626 }
627 })
628}
629
630impl<R: Resource, H: 'static> EffectCx<R, H> {
631 pub(crate) fn new(env: EffectEnv<R>, invocation: Option<InvocationId>) -> Self {
632 Self {
633 env,
634 invocation,
635 started: None,
636 document: None,
637 heads: Vec::new(),
638 custody: None,
639 worker_custody: None,
640 target: None,
641 place: None,
642 host: std::marker::PhantomData,
643 }
644 }
645
646 pub fn place(&self) -> Option<&tilo_resource::Place> {
650 self.place.as_ref()
651 }
652
653 fn with_custody(mut self, custody: Option<ResourceInvocationScope>) -> Result<Self, String> {
654 let (worker, lent) = custody_scopes(custody.as_ref())?;
655 self.worker_custody = worker;
656 if let Some(scope) = lent {
657 self.env.dispatch = self.env.dispatch.for_custody(scope);
658 }
659 self.custody = custody;
660 Ok(self)
661 }
662
663 pub fn document(&self) -> Option<std::cell::Ref<'_, tilo_resource::Doc>> {
669 self.document.as_ref().map(|document| document.borrow())
670 }
671
672 pub fn heads(&self) -> Vec<ui_core::automerge::ChangeHash> {
674 self.heads.clone()
675 }
676
677 pub fn version(&self, version: &str) -> Result<DocumentVersion, String> {
684 let document = self
685 .document
686 .as_ref()
687 .ok_or_else(|| "this effect has no document to read".to_owned())?;
688 let mut doc = document
689 .try_borrow_mut()
690 .map_err(|_| "read a past version before borrowing the document".to_owned())?;
691 let heads = ui_core::history_source::steps::resolve(&doc, version)?;
692 let past = doc
693 .fork_at(&heads)
694 .map_err(|error| format!("version {version:?} is not in this document: {error}"))?;
695 Ok(DocumentVersion {
696 doc: past,
697 name: super::history::version_name(&heads),
698 heads,
699 })
700 }
701
702 pub fn dispatcher(&self) -> Dispatcher<R> {
705 self.env.dispatch.clone()
706 }
707
708 pub fn apply(&self, action: &R::Action) {
710 self.env.dispatch.apply(action);
711 }
712
713 pub fn submit(&self, submit: impl super::Submit<R>) {
715 self.env.dispatch.submit(submit);
716 }
717
718 pub fn live(&self) -> &Live<R> {
720 &self.env.live
721 }
722
723 pub fn invocation(&self) -> Option<InvocationId> {
727 self.invocation
728 }
729
730 pub fn approved_check(
737 &self,
738 check_id: &str,
739 ) -> Result<ui_core::invocation_scope::ApprovedCheckFuture, String> {
740 if let Some(custody) = &self.custody {
741 return custody.approved_check(check_id);
742 }
743 let target = self
744 .target
745 .as_ref()
746 .ok_or("approved checks require an exact dispatched record target")?;
747 if target.route().app_id != self.env.dispatch.app_id() {
748 return Err("approved check dispatch app changed".into());
749 }
750 target.approved_check(
751 self.invocation
752 .ok_or("approved checks require a dispatched invocation")?,
753 check_id,
754 )
755 }
756
757 pub fn approved_check_reported(
763 &self,
764 check_id: &str,
765 ) -> Result<
766 impl FnOnce(Progress) -> ui_core::invocation_scope::ApprovedCheckFuture
767 + Send
768 + 'static
769 + use<R, H>,
770 String,
771 > {
772 let checked = if let Some(custody) = &self.custody {
773 custody.approved_check_reported(check_id)?
774 } else {
775 let target = self
776 .target
777 .as_ref()
778 .ok_or("approved checks require an exact dispatched record target")?;
779 if target.route().app_id != self.env.dispatch.app_id() {
780 return Err("approved check dispatch app changed".into());
781 }
782 target.approved_check_reported(
783 self.invocation
784 .ok_or("approved checks require a dispatched invocation")?,
785 check_id,
786 )?
787 };
788 Ok(move |progress: Progress| attach_approved_check_reporter(checked, progress))
789 }
790
791 pub fn waker(&self) -> WakeHandle {
793 self.env.wake.clone()
794 }
795
796 fn track(&mut self, then: Option<Box<dyn Any>>) -> Progress {
797 track_job(
798 &mut self.started,
799 &self.env.wake,
800 JobCustody::of(self.worker_custody.as_ref(), self.custody.as_ref()),
801 then,
802 std::any::TypeId::of::<Then<R, H>>(),
803 )
804 }
805
806 fn then<T: Send + 'static>(
807 then: impl FnOnce(&mut H, T, &mut Self) -> EffectAnswer + 'static,
808 ) -> Box<dyn Any> {
809 let then: Then<R, H> = Box::new(move |host, value, cx| match value.downcast::<T>() {
810 Ok(value) => then(host, *value, cx),
811 Err(_) => Err("a job finished with a value of another type".into()),
812 });
813 Box::new(then)
814 }
815
816 pub fn spawn<T, F>(
820 &mut self,
821 work: impl FnOnce(Progress) -> F,
822 then: impl FnOnce(&mut H, T, &mut Self) -> EffectAnswer + 'static,
823 ) -> Effected
824 where
825 T: Send + 'static,
826 F: std::future::Future<Output = T> + Send + 'static,
827 {
828 let progress = self.track(Some(Self::then(then)));
829 let future = work(progress.clone());
830 let task = spawn_future(
831 progress,
832 Box::pin(async move { Box::new(future.await) as Box<dyn Any + Send> }),
833 );
834 if let Some(started) = self.started.as_mut() {
835 started.core.task = Some(task);
836 }
837 Effected::Pending
838 }
839
840 pub fn thread<T>(
844 &mut self,
845 work: impl FnOnce(Progress) -> T + Send + 'static,
846 then: impl FnOnce(&mut H, T, &mut Self) -> EffectAnswer + 'static,
847 ) -> Effected
848 where
849 T: Send + 'static,
850 {
851 let progress = self.track(Some(Self::then(then)));
852 let task = spawn_work(
853 progress,
854 Box::new(move |progress| Box::new(work(progress)) as Box<dyn Any + Send>),
855 );
856 if let Some(started) = self.started.as_mut() {
857 started.core.task = Some(task);
858 }
859 Effected::Pending
860 }
861
862 pub fn reply(&mut self) -> EffectReply {
865 let progress = self.track(None);
866 EffectReply {
867 progress,
868 completion: false,
869 }
870 }
871
872 pub fn reply_then(
877 &mut self,
878 then: impl FnOnce(&mut H, ActionOutput, &mut Self) -> EffectAnswer + 'static,
879 ) -> EffectReply {
880 let progress = self.track(Some(Self::then(then)));
881 EffectReply {
882 progress,
883 completion: true,
884 }
885 }
886
887 pub fn when_landed(
905 &mut self,
906 then: impl FnOnce(&mut H, super::Landed, &mut Self) -> EffectAnswer + 'static,
907 ) -> EffectAnswer {
908 let invocation = self
909 .invocation
910 .ok_or("only an invoked effect waits for what it submitted")?;
911 let receiver = self
912 .env
913 .dispatch
914 .wait_landed(invocation)
915 .ok_or("the effect submitted nothing to wait for")?;
916 Ok(self.wait_for(receiver, |host, landed, cx| match landed {
917 Some(Ok(landed)) => then(host, landed, cx),
918 Some(Err(refused)) => Err(refused),
919 None => Err("the submission was dropped before it landed".into()),
920 }))
921 }
922
923 fn wait_for<T: Send + 'static>(
928 &mut self,
929 receiver: Receiver<T>,
930 then: impl FnOnce(&mut H, Option<T>, &mut Self) -> EffectAnswer + 'static,
931 ) -> Effected {
932 let progress = self.track(Some(Self::then(then)));
933 if let Some(started) = self.started.as_mut() {
934 started.core.source = Some(Box::new(move || {
935 let _waiting = &progress;
937 match receiver.try_recv() {
938 Ok(value) => Some(Box::new(Some(value)) as Box<dyn Any + Send>),
939 Err(TryRecvError::Empty) => None,
940 Err(TryRecvError::Disconnected) => Some(Box::new(None::<T>)),
941 }
942 }));
943 }
944 Effected::Pending
945 }
946
947 pub fn choose_file(
954 &mut self,
955 request: PickRequest,
956 then: impl FnOnce(&mut H, Option<ChosenFile>, &mut Self) -> EffectAnswer + 'static,
957 ) -> Effected {
958 let chosen = request.send_contents();
959 self.wait_for(chosen, move |host, outcome, cx| match outcome {
960 Some(ContentsOutcome::Picked(files)) => {
961 then(host, files.into_iter().next().map(ChosenFile::from), cx)
962 }
963 Some(ContentsOutcome::Cancelled) => then(host, None, cx),
964 Some(ContentsOutcome::Failed(reason)) => Err(reason),
965 None => Err(CHOOSER_GONE.into()),
966 })
967 }
968
969 pub fn choose_files(
974 &mut self,
975 request: PickRequest,
976 then: impl FnOnce(&mut H, Vec<ChosenFile>, &mut Self) -> EffectAnswer + 'static,
977 ) -> Effected {
978 let chosen = request.send_contents();
979 self.wait_for(chosen, move |host, outcome, cx| match outcome {
980 Some(ContentsOutcome::Picked(files)) => {
981 then(host, files.into_iter().map(ChosenFile::from).collect(), cx)
982 }
983 Some(ContentsOutcome::Cancelled) => then(host, Vec::new(), cx),
984 Some(ContentsOutcome::Failed(reason)) => Err(reason),
985 None => Err(CHOOSER_GONE.into()),
986 })
987 }
988
989 pub fn choose_path(
996 &mut self,
997 request: PickRequest,
998 then: impl FnOnce(&mut H, Option<std::path::PathBuf>, &mut Self) -> EffectAnswer + 'static,
999 ) -> Effected {
1000 let chosen = request.send();
1001 self.wait_for(chosen, move |host, outcome, cx| match outcome {
1002 Some(PortalOutcome::Opened(path)) => then(host, Some(path), cx),
1003 Some(PortalOutcome::Cancelled) => then(host, None, cx),
1004 Some(PortalOutcome::Failed(reason)) => Err(reason),
1005 None => Err(CHOOSER_GONE.into()),
1006 })
1007 }
1008
1009 pub fn save_file(
1015 &mut self,
1016 request: PickRequest,
1017 bytes: Vec<u8>,
1018 then: impl FnOnce(&mut H, bool, &mut Self) -> EffectAnswer + 'static,
1019 ) -> Effected {
1020 let saved = request.send_save_contents(bytes);
1021 self.wait_for(saved, move |host, outcome, cx| match outcome {
1022 Some(SaveOutcome::Saved) => then(host, true, cx),
1023 Some(SaveOutcome::Cancelled) => then(host, false, cx),
1024 Some(SaveOutcome::Failed(reason)) => Err(reason),
1025 None => Err(CHOOSER_GONE.into()),
1026 })
1027 }
1028
1029 pub fn open_here(
1037 &mut self,
1038 opener: &super::OpenHere,
1039 arguments: &[String],
1040 then: impl FnOnce(&mut H, Result<(), String>, &mut Self) -> EffectAnswer + 'static,
1041 ) -> Effected {
1042 struct Asked(ui_core::application::ReplacementOutcome);
1044 impl Drop for Asked {
1045 fn drop(&mut self) {
1046 self.0.withdraw();
1047 }
1048 }
1049 let asked = Asked(opener.open(arguments));
1050 let progress = self.track(Some(Self::then(then)));
1051 if let Some(started) = self.started.as_mut() {
1052 started.core.source = Some(Box::new(move || {
1053 let _waiting = &progress;
1055 asked
1056 .0
1057 .get()
1058 .map(|answer| Box::new(answer) as Box<dyn Any + Send>)
1059 }));
1060 }
1061 Effected::Pending
1062 }
1063}
1064
1065pub struct Jobs<R: Resource> {
1077 env: Option<EffectEnv<R>>,
1078 queue: Rc<RefCell<JobQueue>>,
1079}
1080
1081impl<R: Resource> Clone for Jobs<R> {
1082 fn clone(&self) -> Self {
1083 Self {
1084 env: self.env.clone(),
1085 queue: Rc::clone(&self.queue),
1086 }
1087 }
1088}
1089
1090#[derive(Clone, Debug, PartialEq, Eq)]
1092pub struct JobProgress {
1093 pub phase: String,
1094 pub completed: Option<u64>,
1095 pub total: Option<u64>,
1096}
1097
1098#[derive(Default)]
1100struct JobQueue {
1101 started: Vec<(String, Job)>,
1103 cancelled: Vec<String>,
1104 running: std::collections::HashSet<String>,
1105 progress: HashMap<String, JobProgress>,
1107}
1108
1109impl<R: Resource> Jobs<R> {
1110 pub(crate) fn new(env: Option<EffectEnv<R>>) -> Self {
1111 Self {
1112 env,
1113 queue: Rc::default(),
1114 }
1115 }
1116
1117 pub fn start<H: 'static>(
1122 &self,
1123 key: impl Into<String>,
1124 start: impl FnOnce(&mut EffectCx<R, H>) -> EffectAnswer,
1125 ) {
1126 let Some(env) = &self.env else {
1127 return;
1128 };
1129 let mut cx = EffectCx::<R, H>::new(env.clone(), None);
1130 let answer = start(&mut cx);
1131 self.queue
1132 .borrow_mut()
1133 .started(key.into(), answer, cx.started.take(), R::KIND);
1134 }
1135
1136 pub fn cancel(&self, key: &str) {
1138 let mut queue = self.queue.borrow_mut();
1139 queue.started.retain(|(queued, _)| queued != key);
1140 queue.progress.remove(key);
1141 if queue.running.remove(key) {
1142 queue.cancelled.push(key.to_owned());
1143 }
1144 }
1145
1146 pub fn is_running(&self, key: &str) -> bool {
1148 self.queue.borrow().running.contains(key)
1149 }
1150
1151 pub fn progress(&self, key: &str) -> Option<JobProgress> {
1154 self.queue.borrow().progress.get(key).cloned()
1155 }
1156}
1157
1158pub(crate) struct OwnedJobs<R: Resource, H> {
1160 jobs: Jobs<R>,
1161 running: Running<String>,
1162 marker: std::marker::PhantomData<fn(&mut H)>,
1163}
1164
1165impl<R: Resource, H: 'static> OwnedJobs<R, H> {
1166 pub(crate) fn new(jobs: Jobs<R>) -> Self {
1167 Self {
1168 jobs,
1169 running: Running::default(),
1170 marker: std::marker::PhantomData,
1171 }
1172 }
1173
1174 pub(crate) fn poll(&mut self, host: &mut H) {
1177 let Some(env) = self.jobs.env.clone() else {
1178 return;
1179 };
1180 poll_owned(
1181 &self.jobs.queue,
1182 &mut self.running,
1183 std::any::TypeId::of::<Then<R, H>>(),
1184 &mut |then, value, custody, invocation| {
1185 complete::<R, H>(&env, host, then, value, custody, invocation)
1186 },
1187 );
1188 }
1189}
1190
1191fn poll_owned(
1194 queue: &RefCell<JobQueue>,
1195 running: &mut Running<String>,
1196 owner: std::any::TypeId,
1197 complete: &mut Complete<'_>,
1198) {
1199 {
1200 let mut queue = queue.borrow_mut();
1201 for key in std::mem::take(&mut queue.cancelled) {
1202 running.cancel(&key);
1203 }
1204 let mut others = Vec::new();
1205 for (key, started) in std::mem::take(&mut queue.started) {
1206 if started.owner == owner {
1207 running.insert(key, started);
1208 } else {
1209 others.push((key, started));
1210 }
1211 }
1212 queue.started = others;
1213 }
1214 for key in running.keys() {
1215 while let Some(polled) = running.poll(&key, complete, None) {
1216 if !queue.borrow_mut().note(&key, polled) {
1217 break;
1218 }
1219 }
1220 }
1221}
1222
1223impl JobQueue {
1224 fn started(&mut self, key: String, answer: EffectAnswer, started: Option<Job>, kind: &str) {
1228 match (answer, started) {
1229 (Ok(Effected::Pending), Some(started)) => {
1230 self.cancelled.retain(|cancelled| *cancelled != key);
1231 self.started.retain(|(queued, _)| *queued != key);
1232 self.running.insert(key.clone());
1233 self.progress.remove(&key);
1234 self.started.push((key, started));
1235 }
1236 (answer, started) => {
1237 if let Some(started) = started {
1238 started.stop();
1239 }
1240 if let Err(error) = answer {
1241 tracing::warn!(%error, kind, %key, "a job failed to start");
1242 }
1243 }
1244 }
1245 }
1246
1247 fn note(&mut self, key: &str, polled: Polled) -> bool {
1249 match polled {
1250 Polled::Progress {
1251 phase,
1252 completed,
1253 total,
1254 ..
1255 } => {
1256 self.progress.insert(
1257 key.to_owned(),
1258 JobProgress {
1259 phase,
1260 completed,
1261 total,
1262 },
1263 );
1264 true
1265 }
1266 Polled::Finished(None) => false,
1267 Polled::Finished(Some(result)) => {
1268 if let Err(error) = result {
1269 tracing::warn!(error = %error.message, %key, "a job failed");
1270 }
1271 self.running.remove(key);
1272 self.progress.remove(key);
1273 false
1274 }
1275 }
1276 }
1277}
1278
1279#[derive(Clone, Debug, PartialEq)]
1283pub struct EffectStatus {
1284 id: &'static str,
1285 availability: Option<Availability>,
1286 label: Option<String>,
1287}
1288
1289impl EffectStatus {
1290 pub fn unavailable<E, A>(
1293 effect: ActionRef<E, A>,
1294 code: impl Into<String>,
1295 reason: impl Into<String>,
1296 ) -> Self {
1297 Self {
1298 id: effect.id,
1299 availability: Some(Availability::Unavailable {
1300 code: code.into(),
1301 reason: reason.into(),
1302 }),
1303 label: None,
1304 }
1305 }
1306
1307 pub fn labelled<E, A>(effect: ActionRef<E, A>, label: impl Into<String>) -> Self {
1309 Self {
1310 id: effect.id,
1311 availability: None,
1312 label: Some(label.into()),
1313 }
1314 }
1315
1316 #[must_use]
1318 pub fn with_label(mut self, label: impl Into<String>) -> Self {
1319 self.label = Some(label.into());
1320 self
1321 }
1322
1323 pub(crate) fn id(&self) -> &'static str {
1325 self.id
1326 }
1327
1328 pub(crate) fn overlay(&self, spec: &mut ActionSpec) {
1330 if let Some(availability) = &self.availability {
1331 spec.contract.availability = availability.clone();
1332 }
1333 if let Some(label) = &self.label {
1334 spec.label.clone_from(label);
1335 }
1336 }
1337}
1338
1339pub(crate) fn published_specs<E: Effect>(statuses: &[EffectStatus]) -> Vec<ActionSpec> {
1341 E::specs()
1342 .into_iter()
1343 .map(|mut spec| {
1344 let id = spec.id.clone();
1345 for status in statuses.iter().filter(|status| status.id == id) {
1346 status.overlay(&mut spec);
1347 }
1348 spec
1349 })
1350 .collect()
1351}
1352
1353enum Polled {
1355 Progress {
1356 sequence: u64,
1357 phase: String,
1358 completed: Option<u64>,
1359 total: Option<u64>,
1360 },
1361 Finished(Option<Result<ActionOutput, ActionFailure>>),
1363}
1364
1365pub(crate) struct Running<K = InvocationId> {
1367 running: HashMap<K, Job>,
1368}
1369
1370impl<K> Default for Running<K> {
1371 fn default() -> Self {
1372 Self {
1373 running: HashMap::new(),
1374 }
1375 }
1376}
1377
1378impl<K> Drop for Running<K> {
1379 fn drop(&mut self) {
1380 for (_, job) in self.running.drain() {
1381 job.stop();
1382 }
1383 }
1384}
1385
1386impl<K: Eq + Hash + Clone> Running<K> {
1387 fn insert(&mut self, key: K, started: Job) {
1388 if let Some(previous) = self.running.insert(key, started) {
1389 previous.stop();
1390 }
1391 }
1392
1393 fn contains(&self, key: &K) -> bool {
1394 self.running.contains_key(key)
1395 }
1396
1397 fn keys(&self) -> Vec<K> {
1398 self.running.keys().cloned().collect()
1399 }
1400
1401 fn cancel(&mut self, key: &K) -> bool {
1403 self.running.remove(key).map(Job::stop).is_some()
1404 }
1405
1406 fn poll(
1409 &mut self,
1410 key: &K,
1411 complete: &mut Complete<'_>,
1412 invocation: Option<InvocationId>,
1413 ) -> Option<Polled> {
1414 let value = match self.running.get_mut(key)?.core.next()? {
1415 Next::Progress(progress) => return Some(progress),
1416 Next::Failed(error) => {
1417 self.cancel(key);
1418 return Some(Polled::Finished(Some(Err(error))));
1419 }
1420 Next::Finished(value) => value,
1421 };
1422 let job = self.running.remove(key)?;
1423 let Some(then) = job.then else {
1424 return Some(answered(value));
1425 };
1426 let (answer, next) = match complete(then, value, job.core.custody, invocation) {
1427 Ok(completed) => completed,
1428 Err(error) => return Some(Polled::Finished(Some(Err(failure(error))))),
1429 };
1430 let chained = matches!(answer, Ok(Effected::Pending)) && next.is_some();
1431 match next {
1432 Some(next) if chained => self.insert(key.clone(), next),
1433 Some(next) => next.stop(),
1434 None => {}
1435 }
1436 Some(Polled::Finished(completed(answer, chained)))
1437 }
1438}
1439
1440thread_local! {
1441 static ACTING_FOR: Cell<Option<InvocationId>> = const { Cell::new(None) };
1444}
1445
1446pub(crate) fn acting_for() -> Option<InvocationId> {
1450 ACTING_FOR.get()
1451}
1452
1453struct ActingFor(Option<InvocationId>);
1456
1457impl ActingFor {
1458 fn enter(invocation: Option<InvocationId>) -> Self {
1459 Self(ACTING_FOR.replace(invocation))
1460 }
1461}
1462
1463impl Drop for ActingFor {
1464 fn drop(&mut self) {
1465 ACTING_FOR.set(self.0);
1466 }
1467}
1468
1469fn failure(message: String) -> ActionFailure {
1471 ActionFailure {
1472 code: "effect_failed".into(),
1473 message,
1474 details: None,
1475 retryable: false,
1476 }
1477}
1478
1479fn failed(invocation: InvocationId, error: ActionFailure) -> InvocationUpdate {
1480 InvocationUpdate::Failed { invocation, error }
1481}
1482
1483pub(crate) struct EffectRunner<R: Resource, H, E> {
1485 env: EffectEnv<R>,
1486 jobs: Running,
1487 marker: std::marker::PhantomData<fn(E, &mut H)>,
1488}
1489
1490impl<R: Resource, H: 'static, E: Effect> EffectRunner<R, H, E> {
1491 pub(crate) fn new(env: EffectEnv<R>) -> Self {
1492 Self {
1493 env,
1494 jobs: Running::default(),
1495 marker: std::marker::PhantomData,
1496 }
1497 }
1498
1499 pub(crate) fn start(
1507 &mut self,
1508 op: &ResourceOperation<'_>,
1509 host: &mut H,
1510 doc: Option<&tilo_resource::Doc>,
1511 handle: impl FnOnce(&mut H, E, &mut EffectCx<R, H>) -> EffectAnswer,
1512 ) -> Result<Option<ResourceOperationStart>, String> {
1513 let (request, custody) = op.with_custody();
1514 if request.type_id != E::OPERATION {
1515 return Ok(None);
1516 }
1517 op.check()?;
1518 let effect = E::from_payload(request.payload)?;
1519 let id = effect.id();
1520 let declaration = E::declaration(id);
1521 let place = declaration
1522 .as_ref()
1523 .and_then(|decl| place_of(decl, &effect));
1524 let job = admitted(declaration.as_ref(), id, request.kind)?;
1525 let mut cx = EffectCx::new(self.env.clone(), Some(request.invocation))
1526 .with_custody(owned_scope(custody))?;
1527 cx.target = op.target().cloned();
1528 cx.place.clone_from(&place);
1529 (cx.document, cx.heads) = lend(doc);
1533 let mut handling = Some((handle, effect));
1534 let answer = run_under(
1535 Some(request.invocation),
1536 custody,
1537 &mut || match handling.take() {
1538 Some((handle, effect)) => handle(host, effect, &mut cx),
1539 None => Err("an effect's handler ran twice".into()),
1540 },
1541 );
1542 cx.document = None;
1543 let live = &self.env.live;
1544 let started = settle_start(
1545 &mut self.jobs,
1546 Answered {
1547 id,
1548 invocation: request.invocation,
1549 job,
1550 answer,
1551 started: cx.started.take(),
1552 },
1553 &mut || {
1554 track_job(
1555 &mut cx.started,
1556 &cx.env.wake,
1557 JobCustody::of(cx.worker_custody.as_ref(), cx.custody.as_ref()),
1558 None,
1559 std::any::TypeId::of::<Then<R, H>>(),
1560 )
1561 },
1562 &mut |participant| {
1563 if let Some(place) = &place {
1564 live.land(participant, place.clone());
1565 }
1566 },
1567 );
1568 if let Some(ready) = cx.started.take() {
1569 self.jobs.insert(request.invocation, ready);
1570 }
1571 started.map(Some)
1572 }
1573
1574 pub(crate) fn poll(
1577 &mut self,
1578 invocation: InvocationId,
1579 cancel: bool,
1580 host: &mut H,
1581 ) -> Option<InvocationUpdate> {
1582 let env = &self.env;
1583 poll_invocation(
1584 &mut self.jobs,
1585 invocation,
1586 cancel,
1587 &mut |then, value, custody, invocation| {
1588 complete::<R, H>(env, host, then, value, custody, invocation)
1589 },
1590 )
1591 }
1592}
1593
1594fn custody_scopes(
1597 custody: Option<&ResourceInvocationScope>,
1598) -> Result<(Option<ResourceEffectScope>, Option<ResourceInvocationScope>), String> {
1599 let Some(custody) = custody else {
1600 return Ok((None, None));
1601 };
1602 Ok((Some(custody.effect_scope()?), Some(custody.clone())))
1603}
1604
1605fn lend(
1607 doc: Option<&tilo_resource::Doc>,
1608) -> (
1609 Option<Rc<RefCell<tilo_resource::Doc>>>,
1610 Vec<ui_core::automerge::ChangeHash>,
1611) {
1612 match doc {
1613 Some(doc) => (Some(Rc::new(RefCell::new(doc.clone()))), doc.base_heads()),
1614 None => (None, Vec::new()),
1615 }
1616}
1617
1618fn admitted(
1625 declaration: Option<&tilo_resource::effect::EffectDecl>,
1626 id: &str,
1627 kind: tilo_actions::OperationKind,
1628) -> Result<bool, String> {
1629 if declaration.map(|decl| decl.kind) != Some(kind) {
1630 return Err(format!("`{id}` was invoked as another kind of operation"));
1631 }
1632 Ok(declaration.is_some_and(tilo_resource::effect::EffectDecl::is_job))
1633}
1634
1635struct Answered<'a> {
1637 id: &'a str,
1638 invocation: InvocationId,
1639 job: bool,
1641 answer: EffectAnswer,
1642 started: Option<Job>,
1644}
1645
1646fn settle_start(
1655 jobs: &mut Running,
1656 answered: Answered<'_>,
1657 reply: &mut dyn FnMut() -> Progress,
1658 land: &mut dyn FnMut(tilo_actions::ParticipantId),
1659) -> Result<ResourceOperationStart, String> {
1660 let Answered {
1661 id,
1662 invocation,
1663 job,
1664 answer,
1665 started,
1666 } = answered;
1667 let answer = match answer {
1668 Ok(answer) => answer,
1669 Err(error) => {
1670 if let Some(started) = started {
1671 started.stop();
1672 }
1673 return Err(error);
1674 }
1675 };
1676 if let Some(participant) = ui_core::invocation_scope::current_participant() {
1678 land(participant);
1679 }
1680 match (answer, started, job) {
1681 (Effected::Done(output), started, false) => {
1682 if let Some(started) = started {
1683 started.stop();
1684 }
1685 Ok(ResourceOperationStart::Completed(output))
1686 }
1687 (Effected::Done(output), started, true) => {
1690 if let Some(started) = started {
1691 started.stop();
1692 }
1693 let reply = EffectReply {
1694 progress: reply(),
1695 completion: false,
1696 };
1697 reply.finish(Ok(output));
1698 Ok(ResourceOperationStart::Accepted)
1699 }
1700 (Effected::Pending, Some(started), true) => {
1701 jobs.insert(invocation, started);
1702 Ok(ResourceOperationStart::Accepted)
1703 }
1704 (Effected::Pending, Some(started), false) => {
1705 started.stop();
1706 Err(format!(
1707 "`{id}` is declared to answer at once; declare it `#[effect(job)]` to answer later"
1708 ))
1709 }
1710 (Effected::Pending, None, _) => Err(format!("`{id}` answered later but started no job")),
1711 }
1712}
1713
1714fn poll_invocation(
1718 jobs: &mut Running,
1719 invocation: InvocationId,
1720 cancel: bool,
1721 complete: &mut Complete<'_>,
1722) -> Option<InvocationUpdate> {
1723 if !jobs.contains(&invocation) {
1724 return None;
1725 }
1726 if cancel && jobs.cancel(&invocation) {
1727 return Some(InvocationUpdate::Cancelled {
1728 invocation,
1729 message: None,
1730 });
1731 }
1732 match jobs.poll(&invocation, complete, Some(invocation))? {
1733 Polled::Progress {
1734 sequence,
1735 phase,
1736 completed,
1737 total,
1738 } => Some(InvocationUpdate::Progress {
1739 invocation,
1740 sequence,
1741 phase,
1742 completed,
1743 total,
1744 message: None,
1745 }),
1746 Polled::Finished(None) => None,
1747 Polled::Finished(Some(Ok(output))) => {
1748 Some(InvocationUpdate::Succeeded { invocation, output })
1749 }
1750 Polled::Finished(Some(Err(error))) => Some(failed(invocation, error)),
1751 }
1752}
1753
1754struct Published {
1756 alive: Weak<()>,
1757 operation: &'static str,
1758 specs: Box<dyn Fn() -> Vec<ActionSpec>>,
1759}
1760
1761trait ViewHandler {
1763 fn start(
1764 &mut self,
1765 op: &ResourceOperation<'_>,
1766 ) -> Result<Option<ResourceOperationStart>, String>;
1767 fn poll(&mut self, invocation: InvocationId, cancel: bool) -> Option<InvocationUpdate>;
1768}
1769
1770type Handle<R, E> = Box<dyn FnMut(E, &mut EffectCx<R>) -> EffectAnswer>;
1771
1772struct Handler<R: Resource, E> {
1773 runner: EffectRunner<R, (), E>,
1774 handle: Handle<R, E>,
1775}
1776
1777impl<R: Resource, E: Effect> ViewHandler for Handler<R, E> {
1778 fn start(
1779 &mut self,
1780 op: &ResourceOperation<'_>,
1781 ) -> Result<Option<ResourceOperationStart>, String> {
1782 let handle = &mut self.handle;
1783 self.runner
1784 .start(op, &mut (), None, |(), effect, cx| handle(effect, cx))
1785 }
1786
1787 fn poll(&mut self, invocation: InvocationId, cancel: bool) -> Option<InvocationUpdate> {
1788 self.runner.poll(invocation, cancel, &mut ())
1789 }
1790}
1791
1792#[derive(Clone, Default)]
1800pub struct Projection {
1801 outside_changed: Option<Rc<Cell<bool>>>,
1803}
1804
1805impl Projection {
1806 pub fn outside_changed(&self) {
1810 if let Some(flag) = &self.outside_changed {
1811 flag.set(true);
1812 }
1813 }
1814}
1815
1816impl std::fmt::Debug for Projection {
1817 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1818 f.debug_struct("Projection")
1819 .field("runs", &self.outside_changed.is_some())
1820 .finish()
1821 }
1822}
1823
1824trait Drainer {
1826 fn run(&mut self, state: &dyn Any, revision: u64);
1829 fn poll(&mut self, revision: u64) -> bool;
1831 fn forget_settled(&mut self);
1833}
1834
1835type OwnedEntries = Box<dyn Fn(&dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>>>;
1838type StartEntry = Box<
1841 dyn FnMut(
1842 &Id,
1843 Box<dyn Any>,
1844 Option<InvocationId>,
1845 Option<&ResourceInvocationScope>,
1846 ) -> Result<(EffectAnswer, Option<Job>), String>,
1847>;
1848type SettleEntry = Box<
1852 dyn Fn(
1853 &Id,
1854 Result<ActionOutput, String>,
1855 ) -> Result<Option<(ApplySettlement, Result<ActionOutput, ActionFailure>)>, String>,
1856>;
1857
1858struct OutboxDrain {
1863 kind: &'static str,
1864 owned: OwnedEntries,
1865 start: StartEntry,
1866 settle: SettleEntry,
1867 complete: Box<Complete<'static>>,
1869 abandoned: fn() -> Option<serde_json::Value>,
1871 jobs: Running<Id>,
1872 retry: HashMap<Id, u64>,
1875 settled: std::collections::HashSet<Id>,
1878 caused: HashMap<Id, (InvocationId, causal::Group)>,
1881 revision: u64,
1883 settling: Settling,
1885}
1886
1887struct Settlement {
1891 apply: ApplySettlement,
1892 waiting: Option<(causal::Group, Result<ActionOutput, ActionFailure>)>,
1893}
1894
1895type Settling = Rc<RefCell<Vec<Settlement>>>;
1896type ApplySettlement =
1897 Box<dyn FnOnce(&mut ui_core::widget::Edits<'_>) -> Result<(), tilo_actions::ActionError>>;
1898
1899fn apply_settlements(settling: &Settling, edits: &mut ui_core::widget::Edits<'_>) {
1902 let settling = std::mem::take(&mut *settling.borrow_mut());
1903 for Settlement { apply, waiting } in settling {
1904 let applied = apply(edits);
1905 if let Some((group, told)) = waiting {
1906 group.borrow_mut().finished(match applied {
1907 Ok(()) => told,
1908 Err(error) => Err(failure(error.to_string())),
1909 });
1910 } else if let Err(error) = applied {
1911 tracing::warn!(%error, "an outbox settlement was refused");
1912 }
1913 }
1914}
1915
1916fn settlement<O>(output: &ActionOutput) -> Result<Settled<O>, String>
1920where
1921 O: serde::de::DeserializeOwned + Default,
1922{
1923 let Some(value) = &output.value else {
1924 return Ok(Settled::Done(O::default()));
1925 };
1926 serde_json::from_value::<Settled<O>>(value.clone())
1927 .or_else(|_| serde_json::from_value::<O>(value.clone()).map(Settled::Done))
1928 .map_err(|error| {
1929 format!("an outbox handler answered what its entry does not settle with: {error}")
1930 })
1931}
1932
1933fn abandoned<O: serde::Serialize + Default>() -> Option<serde_json::Value> {
1935 serde_json::to_value(Settled::<O>::failed("cancelled")).ok()
1936}
1937
1938impl OutboxDrain {
1939 fn finish(&mut self, id: &Id, answer: Result<ActionOutput, String>) -> bool {
1942 let group = self.caused.remove(id).map(|(_, group)| group);
1943 match (self.settle)(id, answer) {
1944 Ok(Some((apply, told))) => {
1945 self.settled.insert(id.clone());
1946 self.settling.borrow_mut().push(Settlement {
1949 apply,
1950 waiting: group.map(|group| (group, told)),
1951 });
1952 true
1953 }
1954 Ok(None) => {
1955 self.settled.insert(id.clone());
1956 false
1957 }
1958 Err(error) => {
1959 tracing::warn!(%error, kind = self.kind, "an outbox entry stays pending");
1960 if let Some(group) = group {
1961 group.borrow_mut().finished(Err(failure(error)));
1962 }
1963 self.retry.insert(id.clone(), self.revision);
1964 false
1965 }
1966 }
1967 }
1968
1969 fn owned(&self, state: &dyn Any) -> Option<Vec<(Id, Box<dyn Any>)>> {
1971 (self.owned)(state)
1972 }
1973
1974 fn start_entry(
1977 &mut self,
1978 id: Id,
1979 value: Box<dyn Any>,
1980 invocation: Option<InvocationId>,
1981 custody: Option<&ResourceInvocationScope>,
1982 ) -> Result<(), String> {
1983 let (answer, started) = (self.start)(&id, value, invocation, custody)?;
1984 match (answer, started) {
1985 (Ok(Effected::Pending), Some(started)) => self.jobs.insert(id, started),
1986 (answer, started) => {
1987 if let Some(started) = started {
1988 started.stop();
1989 }
1990 let answer = answer.and_then(|effected| match effected {
1991 Effected::Done(output) => Ok(output),
1992 Effected::Pending => {
1993 Err("an outbox handler answered later without starting a job".into())
1994 }
1995 });
1996 self.finish(&id, answer);
1997 }
1998 }
1999 Ok(())
2000 }
2001}
2002
2003impl Drainer for OutboxDrain {
2004 fn run(&mut self, state: &dyn Any, revision: u64) {
2005 let Some(pending) = self.owned(state) else {
2006 return;
2007 };
2008 self.revision = revision;
2009 self.settled
2010 .retain(|id| pending.iter().any(|(entry, _)| entry == id));
2011 self.retry
2012 .retain(|id, _| pending.iter().any(|(entry, _)| entry == id));
2013 for (id, value) in pending {
2014 if self.jobs.contains(&id)
2015 || self.settled.contains(&id)
2016 || self.retry.get(&id) == Some(&revision)
2017 {
2018 continue;
2019 }
2020 if let Err(error) = self.start_entry(id, value, None, None) {
2021 tracing::warn!(%error, kind = self.kind, "an outbox entry did not start");
2022 }
2023 }
2024 }
2025
2026 fn poll(&mut self, revision: u64) -> bool {
2027 self.revision = revision;
2028 let mut finished = false;
2029 for id in self.jobs.keys() {
2030 let invocation = self.caused.get(&id).map(|(invocation, _)| *invocation);
2031 while let Some(polled) = self.jobs.poll(&id, &mut *self.complete, invocation) {
2032 let Polled::Finished(result) = polled else {
2033 continue;
2034 };
2035 if let Some(result) = result {
2036 self.finish(&id, result.map_err(|error| error.message));
2037 finished = true;
2038 }
2039 break;
2040 }
2041 }
2042 finished
2043 }
2044
2045 fn forget_settled(&mut self) {
2046 self.retry.clear();
2047 }
2048}
2049
2050impl<D: Drainer> Drainer for Rc<RefCell<D>> {
2054 fn run(&mut self, state: &dyn Any, revision: u64) {
2055 self.borrow_mut().run(state, revision);
2056 }
2057
2058 fn poll(&mut self, revision: u64) -> bool {
2059 self.borrow_mut().poll(revision)
2060 }
2061
2062 fn forget_settled(&mut self) {
2063 self.borrow_mut().forget_settled();
2064 }
2065}
2066
2067struct Registered {
2068 alive: Weak<()>,
2069 operation: &'static str,
2070 handler: Box<dyn ViewHandler>,
2071}
2072
2073struct Drains {
2074 alive: Weak<()>,
2075 drain: Box<dyn Drainer>,
2076}
2077
2078#[derive(Default)]
2079struct Inner {
2080 causal: causal::CausalEffects,
2081 published: Vec<Published>,
2082 handlers: Vec<Registered>,
2083 drains: Vec<Drains>,
2084 drained: Option<u64>,
2086 outside_changed: Rc<Cell<bool>>,
2089 settling: Settling,
2091}
2092
2093pub(crate) struct Effects<R: Resource> {
2096 env: Option<EffectEnv<R>>,
2097 inner: Rc<RefCell<Inner>>,
2098 jobs: Rc<RefCell<OwnedJobs<R, ()>>>,
2100}
2101
2102impl<R: Resource> Clone for Effects<R> {
2103 fn clone(&self) -> Self {
2104 Self {
2105 env: self.env.clone(),
2106 inner: Rc::clone(&self.inner),
2107 jobs: Rc::clone(&self.jobs),
2108 }
2109 }
2110}
2111
2112impl<R: Resource> Effects<R> {
2113 pub(crate) fn live(dispatch: Dispatcher<R>, live: Live<R>) -> Self {
2115 let env = EffectEnv {
2116 dispatch,
2117 live,
2118 wake: WakeHandle::new(),
2119 };
2120 Self {
2121 jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(Some(env.clone()))))),
2122 env: Some(env),
2123 inner: Rc::default(),
2124 }
2125 }
2126
2127 pub(crate) fn inert() -> Self {
2130 Self {
2131 env: None,
2132 inner: Rc::default(),
2133 jobs: Rc::new(RefCell::new(OwnedJobs::new(Jobs::new(None)))),
2134 }
2135 }
2136
2137 pub(crate) fn env(&self) -> Option<&EffectEnv<R>> {
2138 self.env.as_ref()
2139 }
2140
2141 #[inline(never)]
2143 pub(crate) fn view_jobs(&self) -> Jobs<R> {
2144 self.jobs.borrow().jobs.clone()
2145 }
2146
2147 #[inline(never)]
2149 pub(crate) fn surface_jobs(&self) -> Jobs<R> {
2150 Jobs::new(self.env.clone())
2151 }
2152
2153 #[inline(never)]
2155 pub(crate) fn poll_jobs(&self) {
2156 self.jobs.borrow_mut().poll(&mut ());
2157 self.inner.borrow_mut().causal.poll();
2158 }
2159
2160 pub(crate) fn publish<E: Effect>(
2163 &self,
2164 alive: Weak<()>,
2165 specs: impl Fn() -> Vec<ActionSpec> + 'static,
2166 ) {
2167 self.inner.borrow_mut().published.push(Published {
2168 alive,
2169 operation: E::OPERATION,
2170 specs: Box::new(specs),
2171 });
2172 }
2173
2174 pub(crate) fn handle<E: Effect>(
2175 &self,
2176 alive: Weak<()>,
2177 handle: impl FnMut(E, &mut EffectCx<R>) -> EffectAnswer + 'static,
2178 ) {
2179 let Some(env) = self.env.clone() else {
2180 return;
2181 };
2182 self.inner.borrow_mut().handlers.push(Registered {
2183 alive,
2184 operation: E::OPERATION,
2185 handler: Box::new(Handler {
2186 runner: EffectRunner::<R, (), E>::new(env),
2187 handle: Box::new(handle),
2188 }),
2189 });
2190 }
2191
2192 pub(crate) fn outbox<T, O>(
2193 &self,
2194 alive: Weak<()>,
2195 outbox: Path<R, Outbox<T, O>>,
2196 owns: impl Fn(&R::State, &T) -> bool + 'static,
2197 run: impl FnMut(&Id, T, &mut EffectCx<R>) -> EffectAnswer + 'static,
2198 awaited: bool,
2199 ) -> Projection
2200 where
2201 R: Resource<State = R> + tilo_resource::Model + 'static,
2202 T: tilo_resource::Model + Clone + 'static,
2203 O: serde::Serialize + serde::de::DeserializeOwned + Default + 'static,
2204 {
2205 let Some(env) = self.env.clone() else {
2206 return Projection::default();
2207 };
2208 let reader = outbox.clone();
2209 let owned: OwnedEntries = Box::new(move |state: &dyn Any| {
2210 let state = state.downcast_ref::<R>()?;
2211 Some(reader.get(state).map_or_else(Vec::new, |outbox| {
2212 outbox
2213 .pending()
2214 .filter(|(_, value)| owns(state, value))
2215 .map(|(id, value)| (id.clone(), Box::new(value.clone()) as Box<dyn Any>))
2216 .collect()
2217 }))
2218 });
2219 let mut run = run;
2220 let starting = env.clone();
2221 let start: StartEntry = Box::new(move |id, value, invocation, custody| {
2222 let Ok(value) = value.downcast::<T>() else {
2223 return Err("an outbox entry is of another type".into());
2224 };
2225 let mut cx =
2226 EffectCx::new(starting.clone(), invocation).with_custody(owned_scope(custody))?;
2227 let mut value = Some(*value);
2228 let answer = run_under(invocation, custody, &mut || match value.take() {
2229 Some(value) => run(id, value, &mut cx),
2230 None => Err("an outbox entry ran twice".into()),
2231 });
2232 Ok((answer, cx.started.take()))
2233 });
2234 let dispatch = env.dispatch.clone();
2235 let settle: SettleEntry = Box::new(move |id, answer| {
2236 let settled = answer.and_then(|output| settlement::<O>(&output))?;
2237 let told = match &settled {
2238 Settled::Done(_) => Ok(ActionOutput::default()),
2239 Settled::Failed { reason, .. } => Err(failure(reason.clone())),
2240 };
2241 let Some(action) = R::edit_action(outbox.settle(id, &settled)) else {
2242 return Ok(None);
2243 };
2244 let dispatch = dispatch.clone();
2245 let apply: ApplySettlement = Box::new(move |edits| dispatch.apply_now(edits, &action));
2246 Ok(Some((apply, told)))
2247 });
2248 let completing = env;
2249 let complete: Box<Complete<'static>> = Box::new(move |then, value, custody, invocation| {
2250 complete::<R, ()>(&completing, &mut (), then, value, custody, invocation)
2251 });
2252 let drain = Rc::new(RefCell::new(OutboxDrain {
2253 kind: R::KIND,
2254 owned,
2255 start,
2256 settle,
2257 complete,
2258 abandoned: abandoned::<O>,
2259 jobs: Running::default(),
2260 retry: HashMap::new(),
2261 settled: std::collections::HashSet::new(),
2262 caused: HashMap::new(),
2263 revision: 0,
2264 settling: Rc::clone(&self.inner.borrow().settling),
2265 }));
2266 let mut inner = self.inner.borrow_mut();
2267 if awaited {
2268 inner
2269 .causal
2270 .register_drainer(alive.clone(), Box::new(Rc::clone(&drain)));
2271 }
2272 inner.drains.push(Drains {
2273 alive,
2274 drain: Box::new(drain),
2275 });
2276 inner.drained = None;
2277 Projection {
2278 outside_changed: Some(Rc::clone(&inner.outside_changed)),
2279 }
2280 }
2281
2282 pub(crate) fn operations(&self) -> Vec<&'static str> {
2284 self.inner.borrow_mut().operations()
2285 }
2286
2287 #[inline(never)]
2289 pub(crate) fn specs(&self) -> Vec<ActionSpec> {
2290 self.inner.borrow_mut().specs()
2291 }
2292
2293 pub(crate) fn start(
2295 &self,
2296 op: &ResourceOperation<'_>,
2297 ) -> Result<Option<ResourceOperationStart>, String> {
2298 self.inner.borrow_mut().start(op)
2299 }
2300
2301 pub(crate) fn poll(&self, invocation: InvocationId, cancel: bool) -> Option<InvocationUpdate> {
2302 self.inner.borrow_mut().poll(invocation, cancel)
2303 }
2304
2305 pub(crate) fn has_causal_drains(&self) -> bool {
2306 self.inner.borrow().causal.registered()
2307 }
2308
2309 pub(crate) fn start_document_effects(
2310 &self,
2311 operation: &ResourceOperation<'_>,
2312 applied: &ui_core::widget::document_effects::AppliedDocument<'_>,
2313 ) -> Result<Option<ResourceOperationStart>, String> {
2314 self.inner
2315 .borrow_mut()
2316 .causal
2317 .start(operation, applied, read_state::<R>)
2318 }
2319
2320 #[inline(never)]
2323 pub(crate) fn apply_settlements(&self, edits: &mut ui_core::widget::Edits<'_>) {
2324 let settling = Rc::clone(&self.inner.borrow().settling);
2325 apply_settlements(&settling, edits);
2326 }
2327
2328 #[inline(never)]
2331 pub(crate) fn drain_projections(&self, state: &R::State, revision: u64) {
2332 self.inner.borrow_mut().drain_projections(state, revision);
2333 }
2334}
2335
2336fn read_state<R: Resource>(doc: &tilo_resource::Doc) -> Result<Box<dyn Any>, String> {
2338 R::read(doc)
2339 .map(|state| Box::new(state) as Box<dyn Any>)
2340 .map_err(|error| error.to_string())
2341}
2342
2343impl Inner {
2344 fn prune(&mut self) {
2345 self.published
2346 .retain(|entry| entry.alive.strong_count() > 0);
2347 self.handlers.retain(|entry| entry.alive.strong_count() > 0);
2348 self.drains.retain(|entry| entry.alive.strong_count() > 0);
2349 }
2350
2351 fn operations(&mut self) -> Vec<&'static str> {
2352 self.prune();
2353 self.published.iter().map(|entry| entry.operation).collect()
2354 }
2355
2356 fn specs(&mut self) -> Vec<ActionSpec> {
2357 self.prune();
2358 self.published
2359 .iter()
2360 .flat_map(|entry| (entry.specs)())
2361 .collect()
2362 }
2363
2364 fn start(
2365 &mut self,
2366 op: &ResourceOperation<'_>,
2367 ) -> Result<Option<ResourceOperationStart>, String> {
2368 self.prune();
2369 for entry in &mut self.handlers {
2370 if op.with_custody().0.type_id == entry.operation
2371 && let Some(started) = entry.handler.start(op)?
2372 {
2373 return Ok(Some(started));
2374 }
2375 }
2376 Ok(None)
2377 }
2378
2379 fn poll(&mut self, invocation: InvocationId, cancel: bool) -> Option<InvocationUpdate> {
2380 if let Some(update) = self.causal.poll_invocation(invocation, cancel) {
2381 return Some(update);
2382 }
2383 self.handlers
2384 .iter_mut()
2385 .find_map(|entry| entry.handler.poll(invocation, cancel))
2386 }
2387
2388 fn drain_projections(&mut self, state: &dyn Any, revision: u64) {
2389 if self.drains.is_empty() {
2390 return;
2391 }
2392 self.prune();
2393 let mut settled = false;
2394 for entry in &mut self.drains {
2395 settled |= entry.drain.poll(revision);
2396 }
2397 let outside_changed = self.outside_changed.replace(false);
2398 if !settled && !outside_changed && self.drained == Some(revision) {
2399 return;
2400 }
2401 self.drained = Some(revision);
2402 for entry in &mut self.drains {
2403 if outside_changed {
2404 entry.drain.forget_settled();
2405 }
2406 entry.drain.run(state, revision);
2407 }
2408 }
2409}
2410
2411#[cfg(test)]
2412mod tests {
2413 use super::*;
2414 use tilo_resource::{Model, Reader, resource};
2415
2416 #[derive(Model, Default, Debug, Clone)]
2417 pub struct Probe {
2418 pub count: i64,
2419 }
2420
2421 #[resource(kind = "test.probe", label = "Probe")]
2422 impl Probe {
2423 #[action]
2425 pub fn set(&mut self, count: i64) {
2426 self.count = count;
2427 }
2428 }
2429
2430 #[derive(Clone, Debug, tilo_resource::Effect)]
2431 #[effect(operation = "test.probe.effects")]
2432 enum Work {
2433 #[effect(cancellable)]
2435 Count,
2436 Ping,
2438 }
2439
2440 fn runner() -> EffectRunner<Probe, (), Work> {
2441 runner_on(Dispatcher::new("test", Probe::KIND, false, None))
2442 }
2443
2444 fn runner_on(dispatch: Dispatcher<Probe>) -> EffectRunner<Probe, (), Work> {
2445 let mut doc = tilo_resource::Doc::new();
2446 Probe::initialize(&mut doc).unwrap();
2447 EffectRunner::new(EffectEnv {
2448 dispatch,
2449 live: Live::new(Reader::<Probe>::read(&doc).unwrap()),
2450 wake: WakeHandle::detached(),
2451 })
2452 }
2453
2454 #[test]
2455 fn what_an_effect_submits_continues_its_invocation() {
2456 let dispatch = Dispatcher::new("test", Probe::KIND, false, None);
2457 let mut runner = runner_on(dispatch.clone());
2458 let invocation = InvocationId::new();
2459 let payload = payload(&Work::Count);
2460 let op = ResourceOperation::local(
2461 invocation,
2462 tilo_actions::OperationKind::Effect,
2463 Work::OPERATION,
2464 &payload,
2465 );
2466 runner
2467 .start(&op, &mut (), None, |(), _, cx| {
2468 cx.dispatcher().effect(&Work::Ping);
2469 Ok(cx.thread(
2470 |_| (),
2471 |(), (), cx| {
2472 cx.dispatcher().effect(&Work::Ping);
2473 Ok(Effected::done())
2474 },
2475 ))
2476 })
2477 .unwrap();
2478 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
2479 while !runner
2480 .poll(invocation, false, &mut ())
2481 .is_some_and(|update| update.is_terminal())
2482 {
2483 assert!(std::time::Instant::now() < deadline, "the job finishes");
2484 std::thread::yield_now();
2485 }
2486 dispatch.effect(&Work::Ping);
2487 let continues = std::iter::from_fn(|| dispatch.core().take_attributed())
2488 .map(|(_, on_behalf_of)| on_behalf_of)
2489 .collect::<Vec<_>>();
2490 assert_eq!(
2491 continues,
2492 [Some(invocation), Some(invocation), None],
2493 "the handler's and the completion's continue the invocation; \
2494 the app's own continues nothing"
2495 );
2496 }
2497
2498 fn payload(effect: &Work) -> Vec<u8> {
2499 Work::specs()
2500 .into_iter()
2501 .find(|spec| spec.id == effect.id())
2502 .unwrap()
2503 .build_json(&effect.arguments())
2504 .unwrap()
2505 .payload
2506 }
2507
2508 #[test]
2509 fn approved_check_reports_use_original_effect_progress_and_freeze_after_cancel() {
2510 let mut runner = runner();
2511 let invocation = InvocationId::new();
2512 let payload = payload(&Work::Count);
2513 let op = ResourceOperation::local(
2514 invocation,
2515 tilo_actions::OperationKind::Effect,
2516 Work::OPERATION,
2517 &payload,
2518 );
2519 let (forward, reports) = channel();
2520 let checked: ui_core::invocation_scope::ApprovedCheckReportedWork =
2521 Box::new(move |report| {
2522 forward.send(report).unwrap();
2523 Box::pin(std::future::pending())
2524 });
2525 let started = runner
2526 .start(&op, &mut (), None, |(), _, cx| {
2527 Ok(cx.spawn(
2528 move |progress| attach_approved_check_reporter(checked, progress),
2529 |(), _receipt, _| Ok(Effected::Done(ActionOutput::default())),
2530 ))
2531 })
2532 .unwrap();
2533 assert!(matches!(started, Some(ResourceOperationStart::Accepted)));
2534 let report = reports
2535 .recv_timeout(std::time::Duration::from_secs(5))
2536 .unwrap();
2537 for (sequence, phase) in [(1, "Synthesis"), (2, "Routing")] {
2538 report(
2539 ui_core::invocation_scope::ApprovedCheckReportObservation::Reported {
2540 phase: phase.into(),
2541 completed: Some(sequence),
2542 total: Some(2),
2543 },
2544 )
2545 .unwrap();
2546 assert!(matches!(runner.poll(invocation, false, &mut ()),
2547 Some(InvocationUpdate::Progress { invocation: actual, sequence: observed,
2548 phase: actual_phase, completed: Some(done), total: Some(2), message: None })
2549 if actual == invocation && observed == sequence && actual_phase == phase && done == sequence));
2550 assert!(runner.poll(invocation, false, &mut ()).is_none());
2551 }
2552 assert!(matches!(runner.poll(invocation, true, &mut ()),
2553 Some(InvocationUpdate::Cancelled { invocation: actual, .. }) if actual == invocation));
2554 assert!(
2555 report(
2556 ui_core::invocation_scope::ApprovedCheckReportObservation::Reported {
2557 phase: "Late".into(),
2558 completed: None,
2559 total: None,
2560 }
2561 )
2562 .is_err()
2563 );
2564 assert!(runner.poll(invocation, false, &mut ()).is_none());
2565 drop(report);
2566 drop(reports);
2567 drop(runner);
2568 }
2569
2570 #[test]
2571 fn cancelling_a_job_stops_its_worker_and_drops_its_result() {
2572 let mut runner = runner();
2573 let invocation = InvocationId::new();
2574 let payload = payload(&Work::Count);
2575 let op = ResourceOperation::local(
2576 invocation,
2577 tilo_actions::OperationKind::Effect,
2578 Work::OPERATION,
2579 &payload,
2580 );
2581 let (stopped, observed) = channel();
2582 let started = runner
2583 .start(&op, &mut (), None, |(), _, cx| {
2584 Ok(cx.thread(
2585 move |progress| {
2586 while !progress.is_cancelled() {
2587 std::thread::yield_now();
2588 }
2589 let _ = stopped.send(());
2590 7
2591 },
2592 |(), count: i32, _| Ok(Effected::value(count)),
2593 ))
2594 })
2595 .unwrap();
2596 assert!(matches!(started, Some(ResourceOperationStart::Accepted)));
2597 assert!(runner.poll(invocation, false, &mut ()).is_none());
2598 assert!(matches!(
2599 runner.poll(invocation, true, &mut ()),
2600 Some(InvocationUpdate::Cancelled { .. })
2601 ));
2602 assert!(!matches!(
2604 observed.recv_timeout(std::time::Duration::from_secs(30)),
2605 Err(std::sync::mpsc::RecvTimeoutError::Timeout)
2606 ));
2607 assert!(
2608 runner.poll(invocation, false, &mut ()).is_none(),
2609 "a cancelled job's result is dropped"
2610 );
2611 }
2612
2613 #[test]
2614 fn a_failed_handler_cancels_the_job_it_already_started() {
2615 let mut runner = runner();
2616 let invocation = InvocationId::new();
2617 let payload = payload(&Work::Count);
2618 let op = ResourceOperation::local(
2619 invocation,
2620 tilo_actions::OperationKind::Effect,
2621 Work::OPERATION,
2622 &payload,
2623 );
2624 let mut progress = None;
2625 let result = runner.start(&op, &mut (), None, |(), _, cx| {
2626 let reply = cx.reply();
2627 progress = Some(reply.progress().clone());
2628 Err("handler refused after starting work".into())
2629 });
2630 assert!(result.is_err());
2631 assert!(progress.unwrap().is_cancelled());
2632 assert!(runner.poll(invocation, false, &mut ()).is_none());
2633 }
2634
2635 #[test]
2636 fn dropping_the_handler_cancels_its_retained_jobs() {
2637 let mut runner = runner();
2638 let payload = payload(&Work::Count);
2639 let op = ResourceOperation::local(
2640 InvocationId::new(),
2641 tilo_actions::OperationKind::Effect,
2642 Work::OPERATION,
2643 &payload,
2644 );
2645 let mut progress = None;
2646 assert!(matches!(
2647 runner
2648 .start(&op, &mut (), None, |(), _, cx| {
2649 let reply = cx.reply();
2650 progress = Some(reply.progress().clone());
2651 Ok(Effected::Pending)
2652 })
2653 .unwrap(),
2654 Some(ResourceOperationStart::Accepted)
2655 ));
2656 let progress = progress.unwrap();
2657 assert!(!progress.is_cancelled());
2658 drop(runner);
2659 assert!(progress.is_cancelled());
2660 let mut work_ran = false;
2661 assert!(
2662 progress
2663 .with_effect(|| {
2664 work_ran = true;
2665 Ok(())
2666 })
2667 .is_err()
2668 );
2669 assert!(!work_ran, "retired work must not enter its effect callback");
2670 }
2671
2672 #[test]
2675 fn a_handler_reads_the_document_now_and_at_a_version() {
2676 use ui_core::automerge::{ReadDoc, transaction::Transactable};
2677 let mut runner = runner();
2678 let mut doc = tilo_resource::Doc::new();
2679 doc.put(ui_core::automerge::ROOT, "count", 1_i64).unwrap();
2680 let first = tilo_resource::history::commit_stamped(&mut doc).unwrap();
2681 doc.put(ui_core::automerge::ROOT, "count", 2_i64).unwrap();
2682 tilo_resource::history::commit_stamped(&mut doc);
2683 let payload = payload(&Work::Ping);
2684 let op = ResourceOperation::local(
2685 InvocationId::new(),
2686 tilo_actions::OperationKind::Effect,
2687 Work::OPERATION,
2688 &payload,
2689 );
2690 let count = |doc: &tilo_resource::Doc| {
2691 doc.get(ui_core::automerge::ROOT, "count")
2692 .unwrap()
2693 .and_then(|(value, _)| value.as_i64())
2694 };
2695 let before = (doc.base_heads(), doc.pending_ops());
2696 let answered = runner
2697 .start(&op, &mut (), Some(&doc), |(), _, cx| {
2698 let now = count(&cx.document().expect("lent to the handler"));
2699 let past = cx.version(&first.to_string())?;
2700 assert_eq!(past.heads, vec![first]);
2701 Ok(Effected::value([now, count(&past.doc)]))
2702 })
2703 .unwrap();
2704 assert_eq!((doc.base_heads(), doc.pending_ops()), before);
2705 assert!(matches!(
2706 answered,
2707 Some(ResourceOperationStart::Completed(output))
2708 if output.value == Some(serde_json::json!([2, 1]))
2709 ));
2710 assert_eq!(count(&doc), Some(2), "the document is back with its owner");
2711 let refused = runner
2712 .start(&op, &mut (), None, |(), _, cx| {
2713 cx.version(&first.to_string()).map(|_| Effected::done())
2714 })
2715 .unwrap_err();
2716 assert!(refused.contains("no document"), "{refused}");
2717 }
2718
2719 fn opener() -> (
2722 super::super::OpenHere,
2723 Rc<RefCell<Vec<ui_core::application::ReplacementOutcome>>>,
2724 ) {
2725 let asked = Rc::new(RefCell::new(Vec::new()));
2726 let requests = Rc::clone(&asked);
2727 let opener = super::super::OpenHere::new(move |_| {
2728 let outcome = ui_core::application::ReplacementOutcome::default();
2729 requests.borrow_mut().push(outcome.clone());
2730 outcome
2731 });
2732 (opener, asked)
2733 }
2734
2735 fn open_here_job(
2736 runner: &mut EffectRunner<Probe, (), Work>,
2737 opener: &super::super::OpenHere,
2738 ) -> InvocationId {
2739 let invocation = InvocationId::new();
2740 let payload = payload(&Work::Count);
2741 let op = ResourceOperation::local(
2742 invocation,
2743 tilo_actions::OperationKind::Effect,
2744 Work::OPERATION,
2745 &payload,
2746 );
2747 let opener = opener.clone();
2748 runner
2749 .start(&op, &mut (), None, move |(), _, cx| {
2750 Ok(cx.open_here(&opener, &["b.pdf".into()], |(), answer, _| {
2751 answer.map(|()| Effected::value("shown"))
2752 }))
2753 })
2754 .unwrap();
2755 invocation
2756 }
2757
2758 #[test]
2759 fn opening_here_answers_with_the_hosts_answer_or_withdraws_when_cancelled() {
2760 let (opener, asked) = opener();
2761 let mut runner = runner();
2762
2763 let shown = open_here_job(&mut runner, &opener);
2764 assert!(
2765 runner.poll(shown, false, &mut ()).is_none(),
2766 "waits for the host"
2767 );
2768 asked.borrow()[0].answer(Ok(()));
2769 assert!(matches!(
2770 runner.poll(shown, false, &mut ()),
2771 Some(InvocationUpdate::Succeeded { output, .. })
2772 if output.value == Some(serde_json::json!("shown"))
2773 ));
2774
2775 let refused = open_here_job(&mut runner, &opener);
2776 asked.borrow()[1].answer(Err("the file is gone".into()));
2777 assert!(matches!(
2778 runner.poll(refused, false, &mut ()),
2779 Some(InvocationUpdate::Failed { error, .. }) if error.message == "the file is gone"
2780 ));
2781
2782 let cancelled = open_here_job(&mut runner, &opener);
2783 assert!(matches!(
2784 runner.poll(cancelled, true, &mut ()),
2785 Some(InvocationUpdate::Cancelled { .. })
2786 ));
2787 assert!(
2788 !asked.borrow()[2].withdraw(),
2789 "the cancelled effect withdrew its request before the host took it"
2790 );
2791 }
2792
2793 #[test]
2794 fn an_effect_declared_immediate_cannot_answer_later() {
2795 let mut runner = runner();
2796 let payload = payload(&Work::Ping);
2797 let op = ResourceOperation::local(
2798 InvocationId::new(),
2799 tilo_actions::OperationKind::Effect,
2800 Work::OPERATION,
2801 &payload,
2802 );
2803 let refused = runner
2804 .start(&op, &mut (), None, |(), _, cx| {
2805 let _reply = cx.reply();
2806 Ok(Effected::Pending)
2807 })
2808 .unwrap_err();
2809 assert!(refused.contains("#[effect(job)]"), "{refused}");
2810 let answered = runner
2811 .start(&op, &mut (), None, |(), _, _| Ok(Effected::value("pong")))
2812 .unwrap();
2813 assert!(matches!(
2814 answered,
2815 Some(ResourceOperationStart::Completed(output))
2816 if output.value == Some(serde_json::json!("pong"))
2817 ));
2818 }
2819}