1use crate::{
2 action::video::{
3 VideoPause, VideoPlay, VideoSeek, VideoSetMuted, VideoSetRate, VideoSetVolume, VideoStop,
4 },
5 async_runtime::{
6 JobRef, JobRequestPayload, JobSpec, ServiceBindings, ServiceSlot, ServiceSpec,
7 ServiceStartPayload,
8 },
9 context::{Effects, ReducerContext},
10 effect::{ActionInput, Effect, EffectEnvelope},
11 ui::Node,
12 Action, ActionEnvelope, ActionId, ActionScopeId, AppState, BoxedReducer,
13};
14use anyhow::{anyhow, Result};
15use fission_ir::{NodeId, WidgetNodeId};
16use serde::Serialize;
17use std::any::TypeId;
18use std::collections::{BTreeMap, HashMap};
19use std::sync::Arc;
20
21pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
29
30pub trait IntoHandler<S: AppState, A> {
34 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
36}
37
38impl<S: AppState, A> IntoHandler<S, A> for fn(&mut S, A) {
40 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, _ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
41 (self)(state, action);
42 }
43}
44
45impl<S: AppState, A> IntoHandler<S, A>
47 for for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>)
48{
49 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
50 (self)(state, action, ctx);
51 }
52}
53
54type TypedReducer<S> = Box<
56 dyn for<'a, 'b, 'c> Fn(
57 &mut S,
58 &ActionEnvelope,
59 NodeId,
60 &mut Effects<'a, S>,
61 &'b ActionInput,
62 ) -> Result<()>
63 + Send
64 + Sync,
65>;
66
67pub type RawActionHandler<S> = Box<
72 dyn for<'a, 'b> Fn(
73 &mut S,
74 &ActionEnvelope,
75 NodeId,
76 &mut Effects<'a, S>,
77 &'b ActionInput,
78 ) -> Result<()>
79 + Send
80 + Sync,
81>;
82
83pub struct ActionRegistry<S: AppState> {
89 handlers: BTreeMap<ActionId, Vec<TypedReducer<S>>>,
90}
91
92impl<S: AppState> Default for ActionRegistry<S> {
93 fn default() -> Self {
94 Self {
95 handlers: BTreeMap::new(),
96 }
97 }
98}
99
100impl<S: AppState> ActionRegistry<S> {
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn register<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
106 &mut self,
107 handler: H,
108 ) {
109 let action_id = A::static_id();
110
111 let typed_reducer: TypedReducer<S> = Box::new(
112 move |state: &mut S,
113 envelope: &ActionEnvelope,
114 _target,
115 effects,
116 input|
117 -> Result<()> {
118 let action: A = serde_json::from_slice(&envelope.payload)
119 .map_err(|e| anyhow!("Failed to deserialize action: {}", e))?;
120
121 let mut ctx = ReducerContext { effects, input };
122
123 handler.call(state, action, &mut ctx);
124 Ok(())
125 },
126 );
127
128 self.handlers
129 .entry(action_id)
130 .or_default()
131 .push(typed_reducer);
132 }
133
134 pub fn register_raw_action<F>(&mut self, action_id: ActionId, handler: F)
135 where
136 F: for<'a, 'b> Fn(
137 &mut S,
138 &ActionEnvelope,
139 NodeId,
140 &mut Effects<'a, S>,
141 &'b ActionInput,
142 ) -> Result<()>
143 + Send
144 + Sync
145 + 'static,
146 {
147 self.handlers
148 .entry(action_id)
149 .or_default()
150 .push(Box::new(handler));
151 }
152
153 pub fn register_scoped_raw_action<F>(
154 &mut self,
155 scope_id: ActionScopeId,
156 action_id: ActionId,
157 handler: F,
158 ) where
159 F: for<'a, 'b> Fn(
160 &mut S,
161 &ActionEnvelope,
162 NodeId,
163 &mut Effects<'a, S>,
164 &'b ActionInput,
165 ) -> Result<()>
166 + Send
167 + Sync
168 + 'static,
169 {
170 let expected_scope = scope_id.as_u128();
171 self.register_raw_action(action_id, move |state, envelope, target, effects, input| {
172 if input.action_scope_id() == Some(expected_scope) {
173 handler(state, envelope, target, effects, input)
174 } else {
175 Ok(())
176 }
177 });
178 }
179
180 pub fn dispatch_with_input(
181 &mut self,
182 state: &mut S,
183 action: &ActionEnvelope,
184 target: NodeId,
185 input: &ActionInput,
186 ) -> Result<Vec<EffectEnvelope>> {
187 let mut effects_builder = Effects::new_headless(0);
188 if let Some(reducers) = self.handlers.get_mut(&action.id) {
189 for reducer in reducers {
190 reducer(state, action, target, &mut effects_builder, input)?;
191 }
192 }
193 Ok(effects_builder.out)
194 }
195
196 pub fn dispatch(
197 &mut self,
198 state: &mut S,
199 action: &ActionEnvelope,
200 target: NodeId,
201 ) -> Result<Vec<EffectEnvelope>> {
202 self.dispatch_with_input(state, action, target, &ActionInput::None)
203 }
204
205 pub fn into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
206 let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
207 let state_type_id = TypeId::of::<S>();
208
209 for (action_id, typed_reducers) in self.handlers {
210 for typed_reducer in typed_reducers {
211 let boxed_reducer: BoxedReducer = Box::new(
212 move |app_states: &mut HashMap<TypeId, Box<dyn AppState>>,
213 action: &ActionEnvelope,
214 target: NodeId,
215 out_effects: &mut Vec<EffectEnvelope>,
216 input: &ActionInput|
217 -> Result<()> {
218 if let Some(state_box) = app_states.get_mut(&state_type_id) {
219 let concrete_state =
220 state_box.downcast_mut::<S>().ok_or_else(|| {
221 anyhow!("Failed to downcast AppState to concrete type")
222 })?;
223
224 let mut effects_builder = Effects::new_headless(0);
225
226 typed_reducer(
227 concrete_state,
228 action,
229 target,
230 &mut effects_builder,
231 input,
232 )?;
233
234 out_effects.extend(effects_builder.out);
235
236 Ok(())
237 } else {
238 anyhow::bail!("Target AppState for reducer not found in runtime.");
239 }
240 },
241 );
242
243 runtime_reducers
244 .entry(action_id)
245 .or_default()
246 .push(boxed_reducer);
247 }
248 }
249 runtime_reducers
250 }
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, Hash)]
271pub enum AnimationPropertyId {
272 Opacity,
273 TranslateX,
274 TranslateY,
275 Scale,
276 Rotation,
277 Custom(Arc<str>),
278}
279
280impl AnimationPropertyId {
281 pub fn opacity() -> Self {
282 Self::Opacity
283 }
284 pub fn translate_x() -> Self {
285 Self::TranslateX
286 }
287 pub fn translate_y() -> Self {
288 Self::TranslateY
289 }
290 pub fn scale() -> Self {
291 Self::Scale
292 }
293 pub fn rotation() -> Self {
294 Self::Rotation
295 }
296 pub fn custom(name: impl Into<String>) -> Self {
297 Self::Custom(Arc::from(name.into()))
298 }
299 pub fn default_value(&self) -> f32 {
300 match self {
301 Self::Opacity => 1.0,
302 Self::Scale => 1.0,
303 Self::TranslateX | Self::TranslateY | Self::Rotation | Self::Custom(_) => 0.0,
304 }
305 }
306}
307
308#[derive(Clone, Debug)]
310pub enum AnimationStartValue {
311 Explicit(f32),
313 Current,
315}
316
317#[derive(Clone, Debug, PartialEq)]
323pub enum EasingFunction {
324 Linear,
325 EaseIn,
326 EaseOut,
327 EaseInOut,
328 CubicBezier(f32, f32, f32, f32),
329}
330
331impl Default for EasingFunction {
332 fn default() -> Self {
333 Self::EaseInOut
334 }
335}
336
337impl EasingFunction {
338 pub fn apply(&self, t: f32) -> f32 {
340 match self {
341 Self::Linear => t,
342 Self::EaseIn => t * t,
343 Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
344 Self::EaseInOut => {
345 if t < 0.5 {
346 2.0 * t * t
347 } else {
348 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
349 }
350 }
351 Self::CubicBezier(_x1, y1, _x2, y2) => {
352 let t2 = t * t;
354 let t3 = t2 * t;
355 3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
356 }
357 }
358 }
359}
360#[derive(Clone, Debug)]
361pub struct AnimationRequest {
362 pub property: AnimationPropertyId,
364 pub from: AnimationStartValue,
366 pub to: f32,
368 pub duration_ms: u64,
370 pub repeat: bool,
372 pub delay_ms: u64,
374 pub frame_interval_ms: Option<u64>,
379 pub easing: EasingFunction,
380}
381
382#[derive(Clone, Debug)]
385pub struct VideoRegistration {
386 pub node_id: WidgetNodeId,
388 pub source: String,
390 pub autoplay: bool,
392 pub loop_playback: bool,
394}
395
396#[derive(Clone, Debug)]
398pub struct WebRegistration {
399 pub node_id: WidgetNodeId,
401 pub url: String,
403 pub user_agent: Option<String>,
405}
406
407pub struct BuildCtx<S: AppState> {
431 pub registry: ActionRegistry<S>,
433 pub resources: ResourceRegistry,
435 pub animation_requests: Vec<(WidgetNodeId, AnimationRequest)>,
437 pub video_nodes: Vec<VideoRegistration>,
439 pub web_nodes: Vec<WebRegistration>,
441 pub portals: Vec<PortalEntry>,
443 portal_seq: u64,
444}
445
446#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
451pub enum PortalLayer {
452 Default = 0,
454 Modal = 100,
456 Flyout = 200,
458 Toast = 300,
460}
461
462#[derive(Clone, Debug)]
466pub struct PortalEntry {
467 pub layer: PortalLayer,
469 pub seq: u64,
471 pub id: Option<WidgetNodeId>,
473 pub node: Node,
475}
476
477impl<S: AppState> BuildCtx<S> {
478 pub fn new() -> Self {
479 Self {
480 registry: ActionRegistry::new(),
481 resources: ResourceRegistry::new(),
482 animation_requests: Vec::new(),
483 video_nodes: Vec::new(),
484 web_nodes: Vec::new(),
485 portals: Vec::new(),
486 portal_seq: 0,
487 }
488 }
489
490 pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope
491 where
492 H: IntoHandler<S, A> + Send + Sync + 'static,
493 {
494 self.registry.register(handler);
495
496 ActionEnvelope {
497 id: A::static_id(),
498 payload: action.encode(),
499 }
500 }
501
502 pub fn register<A: Action, H>(&mut self, handler: H)
503 where
504 H: IntoHandler<S, A> + Send + Sync + 'static,
505 {
506 self.registry.register::<A, H>(handler);
507 }
508
509 pub fn register_scoped_raw_action<F>(
510 &mut self,
511 scope_id: ActionScopeId,
512 action_id: ActionId,
513 handler: F,
514 ) where
515 F: for<'a, 'b> Fn(
516 &mut S,
517 &ActionEnvelope,
518 NodeId,
519 &mut Effects<'a, S>,
520 &'b ActionInput,
521 ) -> Result<()>
522 + Send
523 + Sync
524 + 'static,
525 {
526 self.registry
527 .register_scoped_raw_action(scope_id, action_id, handler);
528 }
529
530 pub fn request_animation_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
531 self.animation_requests.push((target, request));
532 }
533
534 pub fn register_video(&mut self, registration: VideoRegistration) {
535 self.video_nodes.push(registration);
536 }
537
538 pub fn register_web_view(&mut self, registration: WebRegistration) {
539 self.web_nodes.push(registration);
540 }
541
542 pub fn take_animation_requests(&mut self) -> Vec<(WidgetNodeId, AnimationRequest)> {
543 std::mem::take(&mut self.animation_requests)
544 }
545
546 pub fn take_video_registrations(&mut self) -> Vec<VideoRegistration> {
547 std::mem::take(&mut self.video_nodes)
548 }
549
550 pub fn take_web_registrations(&mut self) -> Vec<WebRegistration> {
551 std::mem::take(&mut self.web_nodes)
552 }
553
554 pub fn take_resources(&mut self) -> Vec<RuntimeResourceDeclaration> {
555 self.resources.take()
556 }
557
558 pub fn register_portal(&mut self, node: Node) {
559 self.register_portal_with_layer(PortalLayer::Default, None, node);
560 }
561
562 pub fn register_portal_with_id(&mut self, id: WidgetNodeId, node: Node) {
563 self.register_portal_with_layer(PortalLayer::Default, Some(id), node);
564 }
565
566 pub fn register_portal_with_layer(
567 &mut self,
568 layer: PortalLayer,
569 id: Option<WidgetNodeId>,
570 node: Node,
571 ) {
572 let seq = self.portal_seq;
573 self.portal_seq = self.portal_seq.wrapping_add(1);
574 self.portals.push(PortalEntry {
575 layer,
576 seq,
577 id,
578 node,
579 });
580 }
581
582 pub fn take_portals(&mut self) -> Vec<(Option<WidgetNodeId>, Node)> {
583 let mut entries = std::mem::take(&mut self.portals);
584 entries.sort_by(|a, b| (a.layer, a.seq).cmp(&(b.layer, b.seq)));
585 entries.into_iter().map(|e| (e.id, e.node)).collect()
586 }
587
588 pub fn anim_for(&mut self, target: WidgetNodeId) -> AnimCtx<'_, S> {
589 AnimCtx { target, ctx: self }
590 }
591
592 pub fn video_controls(&self, target: WidgetNodeId) -> VideoControlCtx {
593 VideoControlCtx { target }
594 }
595}
596
597pub struct AnimCtx<'a, S: AppState> {
598 target: WidgetNodeId,
599 ctx: &'a mut BuildCtx<S>,
600}
601
602impl<'a, S: AppState> AnimCtx<'a, S> {
603 pub fn request(&mut self, request: AnimationRequest) {
604 self.ctx.request_animation_for(self.target, request);
605 }
606
607 pub fn request_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
608 self.ctx.request_animation_for(target, request);
609 }
610}
611
612#[derive(Clone, Copy)]
613pub struct VideoControlCtx {
614 target: WidgetNodeId,
615}
616
617impl VideoControlCtx {
618 pub fn play(&self) -> ActionEnvelope {
619 let action = VideoPlay {
620 target: self.target,
621 };
622 ActionEnvelope {
623 id: VideoPlay::static_id(),
624 payload: action.encode(),
625 }
626 }
627
628 pub fn pause(&self) -> ActionEnvelope {
629 let action = VideoPause {
630 target: self.target,
631 };
632 ActionEnvelope {
633 id: VideoPause::static_id(),
634 payload: action.encode(),
635 }
636 }
637
638 pub fn stop(&self) -> ActionEnvelope {
639 let action = VideoStop {
640 target: self.target,
641 };
642 ActionEnvelope {
643 id: VideoStop::static_id(),
644 payload: action.encode(),
645 }
646 }
647
648 pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
649 let action = VideoSeek {
650 target: self.target,
651 position_ms,
652 };
653 ActionEnvelope {
654 id: VideoSeek::static_id(),
655 payload: action.encode(),
656 }
657 }
658
659 pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
660 let action = VideoSetRate {
661 target: self.target,
662 rate,
663 };
664 ActionEnvelope {
665 id: VideoSetRate::static_id(),
666 payload: action.encode(),
667 }
668 }
669
670 pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
671 let action = VideoSetVolume {
672 target: self.target,
673 volume,
674 };
675 ActionEnvelope {
676 id: VideoSetVolume::static_id(),
677 payload: action.encode(),
678 }
679 }
680
681 pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
682 let action = VideoSetMuted {
683 target: self.target,
684 muted,
685 };
686 ActionEnvelope {
687 id: VideoSetMuted::static_id(),
688 payload: action.encode(),
689 }
690 }
691}
692
693#[derive(Clone, Debug, PartialEq, Eq, Hash)]
694pub struct ResourceKey(String);
695
696impl ResourceKey {
697 pub fn new(name: impl Into<String>) -> Self {
698 Self(name.into())
699 }
700
701 pub fn widget(name: impl AsRef<str>, id: WidgetNodeId) -> Self {
702 Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
703 }
704
705 pub fn as_str(&self) -> &str {
706 &self.0
707 }
708}
709
710#[derive(Clone, Copy, Debug, PartialEq, Eq)]
711pub enum ResourcePolicy {
712 PreserveOnChange,
713 RestartOnChange,
714}
715
716impl Default for ResourcePolicy {
717 fn default() -> Self {
718 Self::RestartOnChange
719 }
720}
721
722#[derive(Clone, Debug, PartialEq)]
723pub struct RuntimeResourceDeclaration {
724 pub key: String,
725 pub deps: Option<Vec<u8>>,
726 pub policy: ResourcePolicy,
727 pub kind: RuntimeResourceKind,
728}
729
730#[derive(Clone, Debug, PartialEq)]
731pub enum RuntimeResourceKind {
732 Job(JobResource),
733 Service(ServiceResource),
734 Timer(TimerResource),
735}
736
737#[derive(Clone, Debug, PartialEq)]
738pub struct JobResource {
739 pub key: ResourceKey,
740 pub effect: EffectEnvelope,
741 pub deps: Option<Vec<u8>>,
742 pub policy: ResourcePolicy,
743}
744
745impl JobResource {
746 pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
747 let payload =
748 serde_json::to_vec(&request).expect("job resource request serialization must succeed");
749 Self {
750 key,
751 effect: EffectEnvelope {
752 req_id: 0,
753 effect: Effect::Job(JobRequestPayload {
754 job_name: job.name.to_string(),
755 payload,
756 }),
757 on_ok: None,
758 on_err: None,
759 service_bindings: None,
760 resource: None,
761 },
762 deps: None,
763 policy: ResourcePolicy::RestartOnChange,
764 }
765 }
766
767 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
768 self.deps =
769 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
770 self
771 }
772
773 pub fn preserve_on_change(mut self) -> Self {
774 self.policy = ResourcePolicy::PreserveOnChange;
775 self
776 }
777
778 pub fn restart_on_change(mut self) -> Self {
779 self.policy = ResourcePolicy::RestartOnChange;
780 self
781 }
782
783 pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
784 self.effect.on_ok = Some(action);
785 self
786 }
787
788 pub fn on_err(mut self, action: ActionEnvelope) -> Self {
789 self.effect.on_err = Some(action);
790 self
791 }
792}
793
794#[derive(Clone, Debug, PartialEq)]
795pub struct ServiceResource {
796 pub key: ResourceKey,
797 pub effect: EffectEnvelope,
798 pub deps: Option<Vec<u8>>,
799 pub policy: ResourcePolicy,
800}
801
802impl ServiceResource {
803 pub fn new<Svc: ServiceSpec>(
804 key: ResourceKey,
805 slot: ServiceSlot<Svc>,
806 config: Svc::Config,
807 ) -> Self {
808 let config = serde_json::to_vec(&config)
809 .expect("service resource config serialization must succeed");
810 Self {
811 key,
812 effect: EffectEnvelope {
813 req_id: 0,
814 effect: Effect::StartService(ServiceStartPayload {
815 service_name: slot.ty.name.to_string(),
816 slot_key: slot.slot_key().to_string(),
817 config,
818 }),
819 on_ok: None,
820 on_err: None,
821 service_bindings: Some(ServiceBindings::default()),
822 resource: None,
823 },
824 deps: None,
825 policy: ResourcePolicy::RestartOnChange,
826 }
827 }
828
829 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
830 self.deps =
831 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
832 self
833 }
834
835 pub fn preserve_on_change(mut self) -> Self {
836 self.policy = ResourcePolicy::PreserveOnChange;
837 self
838 }
839
840 pub fn restart_on_change(mut self) -> Self {
841 self.policy = ResourcePolicy::RestartOnChange;
842 self
843 }
844
845 pub fn on_started(mut self, action: ActionEnvelope) -> Self {
846 if let Some(bindings) = self.effect.service_bindings.as_mut() {
847 bindings.on_started = Some(action);
848 }
849 self
850 }
851
852 pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
853 if let Some(bindings) = self.effect.service_bindings.as_mut() {
854 bindings.on_start_failed = Some(action);
855 }
856 self
857 }
858
859 pub fn on_event(mut self, action: ActionEnvelope) -> Self {
860 if let Some(bindings) = self.effect.service_bindings.as_mut() {
861 bindings.on_event = Some(action);
862 }
863 self
864 }
865
866 pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
867 if let Some(bindings) = self.effect.service_bindings.as_mut() {
868 bindings.on_stopped = Some(action);
869 }
870 self
871 }
872
873 pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
874 if let Some(bindings) = self.effect.service_bindings.as_mut() {
875 bindings.on_command_ok = Some(action);
876 }
877 self
878 }
879
880 pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
881 if let Some(bindings) = self.effect.service_bindings.as_mut() {
882 bindings.on_command_err = Some(action);
883 }
884 self
885 }
886}
887
888#[derive(Clone, Debug, PartialEq, Eq)]
889pub struct TimerResource {
890 pub key: ResourceKey,
891 pub interval_ms: u64,
892 pub payload: Vec<u8>,
893 pub on_tick: Option<ActionEnvelope>,
894 pub deps: Option<Vec<u8>>,
895 pub immediate: bool,
896 pub policy: ResourcePolicy,
897}
898
899impl TimerResource {
900 pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
901 Self {
902 key,
903 interval_ms: interval.as_millis() as u64,
904 payload: serde_json::to_vec(&payload)
905 .expect("timer resource payload serialization must succeed"),
906 on_tick: None,
907 deps: None,
908 immediate: false,
909 policy: ResourcePolicy::RestartOnChange,
910 }
911 }
912
913 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
914 self.deps =
915 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
916 self
917 }
918
919 pub fn preserve_on_change(mut self) -> Self {
920 self.policy = ResourcePolicy::PreserveOnChange;
921 self
922 }
923
924 pub fn restart_on_change(mut self) -> Self {
925 self.policy = ResourcePolicy::RestartOnChange;
926 self
927 }
928
929 pub fn immediate(mut self) -> Self {
930 self.immediate = true;
931 self
932 }
933
934 pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
935 self.on_tick = Some(action);
936 self
937 }
938}
939
940#[derive(Default)]
941pub struct ResourceRegistry {
942 declarations: Vec<RuntimeResourceDeclaration>,
943 seen_keys: HashMap<String, usize>,
944}
945
946impl ResourceRegistry {
947 pub fn new() -> Self {
948 Self::default()
949 }
950
951 pub fn job(&mut self, resource: JobResource) {
952 self.push(RuntimeResourceDeclaration {
953 key: resource.key.as_str().to_string(),
954 deps: resource.deps.clone(),
955 policy: resource.policy,
956 kind: RuntimeResourceKind::Job(resource),
957 });
958 }
959
960 pub fn service(&mut self, resource: ServiceResource) {
961 self.push(RuntimeResourceDeclaration {
962 key: resource.key.as_str().to_string(),
963 deps: resource.deps.clone(),
964 policy: resource.policy,
965 kind: RuntimeResourceKind::Service(resource),
966 });
967 }
968
969 pub fn timer(&mut self, resource: TimerResource) {
970 self.push(RuntimeResourceDeclaration {
971 key: resource.key.as_str().to_string(),
972 deps: resource.deps.clone(),
973 policy: resource.policy,
974 kind: RuntimeResourceKind::Timer(resource),
975 });
976 }
977
978 pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
979 self.seen_keys.clear();
980 std::mem::take(&mut self.declarations)
981 }
982
983 fn push(&mut self, declaration: RuntimeResourceDeclaration) {
984 if let Some(index) = self.seen_keys.get(&declaration.key) {
985 panic!(
986 "duplicate runtime resource declaration for key '{}' at index {}",
987 declaration.key, index
988 );
989 }
990 let index = self.declarations.len();
991 self.seen_keys.insert(declaration.key.clone(), index);
992 self.declarations.push(declaration);
993 }
994}