Skip to main content

fission_core/
registry.rs

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
21/// The canonical 3-argument handler signature for modern reducers.
22///
23/// ```rust,ignore
24/// fn handle_increment(state: &mut Counter, _: Increment, _ctx: &mut ReducerContext<Counter>) {
25///     state.count += 1;
26/// }
27/// ```
28pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
29
30/// Trait that allows both 2-argument (legacy) and 3-argument (modern) handler
31/// functions to be used with [`ActionRegistry::register`] and
32/// [`BuildCtx::bind`].
33pub trait IntoHandler<S: AppState, A> {
34    /// Invoke the handler with the given state, action, and context.
35    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
36}
37
38// Impl for Legacy (2-arg)
39impl<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
45// Impl for Modern (3-arg)
46impl<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
54// Internal typed reducer storage
55type 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
67/// Raw action handler used for mounted or external subtrees.
68///
69/// Unlike typed reducers, this receives the original action envelope and
70/// dispatch target without deserializing the payload.
71pub 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
83/// A per-frame collection of action handlers registered during widget building.
84///
85/// `ActionRegistry` is populated by [`BuildCtx::bind`] calls. After the widget
86/// tree is built, the registry is absorbed into the [`Runtime`](crate::Runtime)
87/// via [`Runtime::absorb_registry`](crate::Runtime::absorb_registry).
88pub 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 into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
181        let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
182        let state_type_id = TypeId::of::<S>();
183
184        for (action_id, typed_reducers) in self.handlers {
185            for typed_reducer in typed_reducers {
186                let boxed_reducer: BoxedReducer = Box::new(
187                    move |app_states: &mut HashMap<TypeId, Box<dyn AppState>>,
188                          action: &ActionEnvelope,
189                          target: NodeId,
190                          out_effects: &mut Vec<EffectEnvelope>,
191                          input: &ActionInput|
192                          -> Result<()> {
193                        if let Some(state_box) = app_states.get_mut(&state_type_id) {
194                            let concrete_state =
195                                state_box.downcast_mut::<S>().ok_or_else(|| {
196                                    anyhow!("Failed to downcast AppState to concrete type")
197                                })?;
198
199                            let mut effects_builder = Effects::new_headless(0);
200
201                            typed_reducer(
202                                concrete_state,
203                                action,
204                                target,
205                                &mut effects_builder,
206                                input,
207                            )?;
208
209                            out_effects.extend(effects_builder.out);
210
211                            Ok(())
212                        } else {
213                            anyhow::bail!("Target AppState for reducer not found in runtime.");
214                        }
215                    },
216                );
217
218                runtime_reducers
219                    .entry(action_id)
220                    .or_default()
221                    .push(boxed_reducer);
222            }
223        }
224        runtime_reducers
225    }
226}
227
228/// Identifies which visual property an animation targets.
229///
230/// Built-in properties have well-known default values (e.g. opacity defaults
231/// to 1.0, translation defaults to 0.0). Custom properties use 0.0.
232///
233/// # Example
234///
235/// ```rust,ignore
236/// ctx.anim_for(widget_id).request(AnimationRequest {
237///     property: AnimationPropertyId::Opacity,
238///     from: AnimationStartValue::Current,
239///     to: 0.0,
240///     duration_ms: 300,
241///     repeat: false,
242///     delay_ms: 0,
243/// });
244/// ```
245#[derive(Clone, Debug, PartialEq, Eq, Hash)]
246pub enum AnimationPropertyId {
247    Opacity,
248    TranslateX,
249    TranslateY,
250    Scale,
251    Rotation,
252    Custom(Arc<str>),
253}
254
255impl AnimationPropertyId {
256    pub fn opacity() -> Self {
257        Self::Opacity
258    }
259    pub fn translate_x() -> Self {
260        Self::TranslateX
261    }
262    pub fn translate_y() -> Self {
263        Self::TranslateY
264    }
265    pub fn scale() -> Self {
266        Self::Scale
267    }
268    pub fn rotation() -> Self {
269        Self::Rotation
270    }
271    pub fn custom(name: impl Into<String>) -> Self {
272        Self::Custom(Arc::from(name.into()))
273    }
274    pub fn default_value(&self) -> f32 {
275        match self {
276            Self::Opacity => 1.0,
277            Self::Scale => 1.0,
278            Self::TranslateX | Self::TranslateY | Self::Rotation | Self::Custom(_) => 0.0,
279        }
280    }
281}
282
283/// Where an animation starts from.
284#[derive(Clone, Debug)]
285pub enum AnimationStartValue {
286    /// Start from an explicit value.
287    Explicit(f32),
288    /// Start from whatever the current animated value is.
289    Current,
290}
291
292/// A request to animate a visual property on a widget.
293///
294/// Registered via [`BuildCtx::request_animation_for`] or
295/// [`AnimCtx::request`].
296/// Easing function applied to animation progress before interpolation.
297#[derive(Clone, Debug, PartialEq)]
298pub enum EasingFunction {
299    Linear,
300    EaseIn,
301    EaseOut,
302    EaseInOut,
303    CubicBezier(f32, f32, f32, f32),
304}
305
306impl Default for EasingFunction {
307    fn default() -> Self {
308        Self::EaseInOut
309    }
310}
311
312impl EasingFunction {
313    /// Map a linear progress value `t` in [0, 1] to an eased value.
314    pub fn apply(&self, t: f32) -> f32 {
315        match self {
316            Self::Linear => t,
317            Self::EaseIn => t * t,
318            Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
319            Self::EaseInOut => {
320                if t < 0.5 {
321                    2.0 * t * t
322                } else {
323                    1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
324                }
325            }
326            Self::CubicBezier(_x1, y1, _x2, y2) => {
327                // Simplified cubic bezier approximation (y-axis only)
328                let t2 = t * t;
329                let t3 = t2 * t;
330                3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t2 * y2 + t3
331            }
332        }
333    }
334}
335#[derive(Clone, Debug)]
336pub struct AnimationRequest {
337    /// The property to animate.
338    pub property: AnimationPropertyId,
339    /// Starting value.
340    pub from: AnimationStartValue,
341    /// Target value.
342    pub to: f32,
343    /// Duration in milliseconds.
344    pub duration_ms: u64,
345    /// Whether to loop the animation.
346    pub repeat: bool,
347    /// Delay before the animation starts (in milliseconds).
348    pub delay_ms: u64,
349    /// Optional preferred redraw cadence for this animation (in milliseconds).
350    ///
351    /// This is primarily useful for low-priority repeating effects such as
352    /// loading indicators that do not need full frame-rate updates.
353    pub frame_interval_ms: Option<u64>,
354    pub easing: EasingFunction,
355}
356
357/// Registration data for a [`Video`](crate::ui::Video) widget collected during
358/// widget building.
359#[derive(Clone, Debug)]
360pub struct VideoRegistration {
361    /// The stable widget identity of the video node.
362    pub node_id: WidgetNodeId,
363    /// URL or asset path to the video file.
364    pub source: String,
365    /// Whether to start playing automatically.
366    pub autoplay: bool,
367    /// Whether to loop playback.
368    pub loop_playback: bool,
369}
370
371/// Registration data for a platform web view collected during widget building.
372#[derive(Clone, Debug)]
373pub struct WebRegistration {
374    /// The stable widget identity of the web view node.
375    pub node_id: WidgetNodeId,
376    /// The URL to load.
377    pub url: String,
378    /// Optional custom user-agent string.
379    pub user_agent: Option<String>,
380}
381
382/// The mutable context passed to [`Widget::build`](crate::Widget::build).
383///
384/// `BuildCtx` is where widgets register side-effects that must survive beyond
385/// the build phase:
386///
387/// - **Action binding** -- [`bind`](BuildCtx::bind) registers a handler and
388///   returns an [`ActionEnvelope`] that can be stored in widget fields like
389///   `on_press`.
390/// - **Portals** -- [`register_portal`](BuildCtx::register_portal) places a
391///   node in the global overlay stack (modals, toasts, flyouts).
392/// - **Animations** -- [`request_animation_for`](BuildCtx::request_animation_for)
393///   or the [`anim_for`](BuildCtx::anim_for) helper.
394/// - **Video / WebView registration** -- [`register_video`](BuildCtx::register_video),
395///   [`register_web_view`](BuildCtx::register_web_view).
396///
397/// # Example
398///
399/// ```rust,ignore
400/// fn build(&self, ctx: &mut BuildCtx<S>, view: &View<S>) -> Node {
401///     let on_press = ctx.bind(MyAction { .. }, reduce_with!(handler));
402///     Button { on_press: Some(on_press), ..Default::default() }.into_node()
403/// }
404/// ```
405pub struct BuildCtx<S: AppState> {
406    /// The action registry that accumulates handlers during the build phase.
407    pub registry: ActionRegistry<S>,
408    /// Declarative runtime resources collected during the build phase.
409    pub resources: ResourceRegistry,
410    /// Pending animation requests.
411    pub animation_requests: Vec<(WidgetNodeId, AnimationRequest)>,
412    /// Registered video nodes.
413    pub video_nodes: Vec<VideoRegistration>,
414    /// Registered web view nodes.
415    pub web_nodes: Vec<WebRegistration>,
416    /// Portal entries (overlays, modals, toasts).
417    pub portals: Vec<PortalEntry>,
418    portal_seq: u64,
419}
420
421/// Z-order layer for portal entries.
422///
423/// Portals are sorted by layer (then by registration order within a layer).
424/// Higher layers paint on top of lower layers.
425#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
426pub enum PortalLayer {
427    /// Default overlay layer.
428    Default = 0,
429    /// Modal dialog layer.
430    Modal = 100,
431    /// Flyout / dropdown layer.
432    Flyout = 200,
433    /// Toast notification layer (topmost).
434    Toast = 300,
435}
436
437/// An entry in the portal overlay stack.
438///
439/// Created by [`BuildCtx::register_portal`] and friends.
440#[derive(Clone, Debug)]
441pub struct PortalEntry {
442    /// Which overlay layer this portal belongs to.
443    pub layer: PortalLayer,
444    /// Insertion order (for stable ordering within a layer).
445    pub seq: u64,
446    /// Optional stable identity.
447    pub id: Option<WidgetNodeId>,
448    /// The portal's widget tree.
449    pub node: Node,
450}
451
452impl<S: AppState> BuildCtx<S> {
453    pub fn new() -> Self {
454        Self {
455            registry: ActionRegistry::new(),
456            resources: ResourceRegistry::new(),
457            animation_requests: Vec::new(),
458            video_nodes: Vec::new(),
459            web_nodes: Vec::new(),
460            portals: Vec::new(),
461            portal_seq: 0,
462        }
463    }
464
465    pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope
466    where
467        H: IntoHandler<S, A> + Send + Sync + 'static,
468    {
469        self.registry.register(handler);
470
471        ActionEnvelope {
472            id: A::static_id(),
473            payload: action.encode(),
474        }
475    }
476
477    pub fn register<A: Action, H>(&mut self, handler: H)
478    where
479        H: IntoHandler<S, A> + Send + Sync + 'static,
480    {
481        self.registry.register::<A, H>(handler);
482    }
483
484    pub fn register_scoped_raw_action<F>(
485        &mut self,
486        scope_id: ActionScopeId,
487        action_id: ActionId,
488        handler: F,
489    ) where
490        F: for<'a, 'b> Fn(
491                &mut S,
492                &ActionEnvelope,
493                NodeId,
494                &mut Effects<'a, S>,
495                &'b ActionInput,
496            ) -> Result<()>
497            + Send
498            + Sync
499            + 'static,
500    {
501        self.registry
502            .register_scoped_raw_action(scope_id, action_id, handler);
503    }
504
505    pub fn request_animation_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
506        self.animation_requests.push((target, request));
507    }
508
509    pub fn register_video(&mut self, registration: VideoRegistration) {
510        self.video_nodes.push(registration);
511    }
512
513    pub fn register_web_view(&mut self, registration: WebRegistration) {
514        self.web_nodes.push(registration);
515    }
516
517    pub fn take_animation_requests(&mut self) -> Vec<(WidgetNodeId, AnimationRequest)> {
518        std::mem::take(&mut self.animation_requests)
519    }
520
521    pub fn take_video_registrations(&mut self) -> Vec<VideoRegistration> {
522        std::mem::take(&mut self.video_nodes)
523    }
524
525    pub fn take_web_registrations(&mut self) -> Vec<WebRegistration> {
526        std::mem::take(&mut self.web_nodes)
527    }
528
529    pub fn take_resources(&mut self) -> Vec<RuntimeResourceDeclaration> {
530        self.resources.take()
531    }
532
533    pub fn register_portal(&mut self, node: Node) {
534        self.register_portal_with_layer(PortalLayer::Default, None, node);
535    }
536
537    pub fn register_portal_with_id(&mut self, id: WidgetNodeId, node: Node) {
538        self.register_portal_with_layer(PortalLayer::Default, Some(id), node);
539    }
540
541    pub fn register_portal_with_layer(
542        &mut self,
543        layer: PortalLayer,
544        id: Option<WidgetNodeId>,
545        node: Node,
546    ) {
547        let seq = self.portal_seq;
548        self.portal_seq = self.portal_seq.wrapping_add(1);
549        self.portals.push(PortalEntry {
550            layer,
551            seq,
552            id,
553            node,
554        });
555    }
556
557    pub fn take_portals(&mut self) -> Vec<(Option<WidgetNodeId>, Node)> {
558        let mut entries = std::mem::take(&mut self.portals);
559        entries.sort_by(|a, b| (a.layer, a.seq).cmp(&(b.layer, b.seq)));
560        entries.into_iter().map(|e| (e.id, e.node)).collect()
561    }
562
563    pub fn anim_for(&mut self, target: WidgetNodeId) -> AnimCtx<'_, S> {
564        AnimCtx { target, ctx: self }
565    }
566
567    pub fn video_controls(&self, target: WidgetNodeId) -> VideoControlCtx {
568        VideoControlCtx { target }
569    }
570}
571
572pub struct AnimCtx<'a, S: AppState> {
573    target: WidgetNodeId,
574    ctx: &'a mut BuildCtx<S>,
575}
576
577impl<'a, S: AppState> AnimCtx<'a, S> {
578    pub fn request(&mut self, request: AnimationRequest) {
579        self.ctx.request_animation_for(self.target, request);
580    }
581
582    pub fn request_for(&mut self, target: WidgetNodeId, request: AnimationRequest) {
583        self.ctx.request_animation_for(target, request);
584    }
585}
586
587#[derive(Clone, Copy)]
588pub struct VideoControlCtx {
589    target: WidgetNodeId,
590}
591
592impl VideoControlCtx {
593    pub fn play(&self) -> ActionEnvelope {
594        let action = VideoPlay {
595            target: self.target,
596        };
597        ActionEnvelope {
598            id: VideoPlay::static_id(),
599            payload: action.encode(),
600        }
601    }
602
603    pub fn pause(&self) -> ActionEnvelope {
604        let action = VideoPause {
605            target: self.target,
606        };
607        ActionEnvelope {
608            id: VideoPause::static_id(),
609            payload: action.encode(),
610        }
611    }
612
613    pub fn stop(&self) -> ActionEnvelope {
614        let action = VideoStop {
615            target: self.target,
616        };
617        ActionEnvelope {
618            id: VideoStop::static_id(),
619            payload: action.encode(),
620        }
621    }
622
623    pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
624        let action = VideoSeek {
625            target: self.target,
626            position_ms,
627        };
628        ActionEnvelope {
629            id: VideoSeek::static_id(),
630            payload: action.encode(),
631        }
632    }
633
634    pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
635        let action = VideoSetRate {
636            target: self.target,
637            rate,
638        };
639        ActionEnvelope {
640            id: VideoSetRate::static_id(),
641            payload: action.encode(),
642        }
643    }
644
645    pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
646        let action = VideoSetVolume {
647            target: self.target,
648            volume,
649        };
650        ActionEnvelope {
651            id: VideoSetVolume::static_id(),
652            payload: action.encode(),
653        }
654    }
655
656    pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
657        let action = VideoSetMuted {
658            target: self.target,
659            muted,
660        };
661        ActionEnvelope {
662            id: VideoSetMuted::static_id(),
663            payload: action.encode(),
664        }
665    }
666}
667
668#[derive(Clone, Debug, PartialEq, Eq, Hash)]
669pub struct ResourceKey(String);
670
671impl ResourceKey {
672    pub fn new(name: impl Into<String>) -> Self {
673        Self(name.into())
674    }
675
676    pub fn widget(name: impl AsRef<str>, id: WidgetNodeId) -> Self {
677        Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
678    }
679
680    pub fn as_str(&self) -> &str {
681        &self.0
682    }
683}
684
685#[derive(Clone, Copy, Debug, PartialEq, Eq)]
686pub enum ResourcePolicy {
687    PreserveOnChange,
688    RestartOnChange,
689}
690
691impl Default for ResourcePolicy {
692    fn default() -> Self {
693        Self::RestartOnChange
694    }
695}
696
697#[derive(Clone, Debug, PartialEq)]
698pub struct RuntimeResourceDeclaration {
699    pub key: String,
700    pub deps: Option<Vec<u8>>,
701    pub policy: ResourcePolicy,
702    pub kind: RuntimeResourceKind,
703}
704
705#[derive(Clone, Debug, PartialEq)]
706pub enum RuntimeResourceKind {
707    Job(JobResource),
708    Service(ServiceResource),
709    Timer(TimerResource),
710}
711
712#[derive(Clone, Debug, PartialEq)]
713pub struct JobResource {
714    pub key: ResourceKey,
715    pub effect: EffectEnvelope,
716    pub deps: Option<Vec<u8>>,
717    pub policy: ResourcePolicy,
718}
719
720impl JobResource {
721    pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
722        let payload =
723            serde_json::to_vec(&request).expect("job resource request serialization must succeed");
724        Self {
725            key,
726            effect: EffectEnvelope {
727                req_id: 0,
728                effect: Effect::Job(JobRequestPayload {
729                    job_name: job.name.to_string(),
730                    payload,
731                }),
732                on_ok: None,
733                on_err: None,
734                service_bindings: None,
735                resource: None,
736            },
737            deps: None,
738            policy: ResourcePolicy::RestartOnChange,
739        }
740    }
741
742    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
743        self.deps =
744            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
745        self
746    }
747
748    pub fn preserve_on_change(mut self) -> Self {
749        self.policy = ResourcePolicy::PreserveOnChange;
750        self
751    }
752
753    pub fn restart_on_change(mut self) -> Self {
754        self.policy = ResourcePolicy::RestartOnChange;
755        self
756    }
757
758    pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
759        self.effect.on_ok = Some(action);
760        self
761    }
762
763    pub fn on_err(mut self, action: ActionEnvelope) -> Self {
764        self.effect.on_err = Some(action);
765        self
766    }
767}
768
769#[derive(Clone, Debug, PartialEq)]
770pub struct ServiceResource {
771    pub key: ResourceKey,
772    pub effect: EffectEnvelope,
773    pub deps: Option<Vec<u8>>,
774    pub policy: ResourcePolicy,
775}
776
777impl ServiceResource {
778    pub fn new<Svc: ServiceSpec>(
779        key: ResourceKey,
780        slot: ServiceSlot<Svc>,
781        config: Svc::Config,
782    ) -> Self {
783        let config = serde_json::to_vec(&config)
784            .expect("service resource config serialization must succeed");
785        Self {
786            key,
787            effect: EffectEnvelope {
788                req_id: 0,
789                effect: Effect::StartService(ServiceStartPayload {
790                    service_name: slot.ty.name.to_string(),
791                    slot_key: slot.slot_key().to_string(),
792                    config,
793                }),
794                on_ok: None,
795                on_err: None,
796                service_bindings: Some(ServiceBindings::default()),
797                resource: None,
798            },
799            deps: None,
800            policy: ResourcePolicy::RestartOnChange,
801        }
802    }
803
804    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
805        self.deps =
806            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
807        self
808    }
809
810    pub fn preserve_on_change(mut self) -> Self {
811        self.policy = ResourcePolicy::PreserveOnChange;
812        self
813    }
814
815    pub fn restart_on_change(mut self) -> Self {
816        self.policy = ResourcePolicy::RestartOnChange;
817        self
818    }
819
820    pub fn on_started(mut self, action: ActionEnvelope) -> Self {
821        if let Some(bindings) = self.effect.service_bindings.as_mut() {
822            bindings.on_started = Some(action);
823        }
824        self
825    }
826
827    pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
828        if let Some(bindings) = self.effect.service_bindings.as_mut() {
829            bindings.on_start_failed = Some(action);
830        }
831        self
832    }
833
834    pub fn on_event(mut self, action: ActionEnvelope) -> Self {
835        if let Some(bindings) = self.effect.service_bindings.as_mut() {
836            bindings.on_event = Some(action);
837        }
838        self
839    }
840
841    pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
842        if let Some(bindings) = self.effect.service_bindings.as_mut() {
843            bindings.on_stopped = Some(action);
844        }
845        self
846    }
847
848    pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
849        if let Some(bindings) = self.effect.service_bindings.as_mut() {
850            bindings.on_command_ok = Some(action);
851        }
852        self
853    }
854
855    pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
856        if let Some(bindings) = self.effect.service_bindings.as_mut() {
857            bindings.on_command_err = Some(action);
858        }
859        self
860    }
861}
862
863#[derive(Clone, Debug, PartialEq, Eq)]
864pub struct TimerResource {
865    pub key: ResourceKey,
866    pub interval_ms: u64,
867    pub payload: Vec<u8>,
868    pub on_tick: Option<ActionEnvelope>,
869    pub deps: Option<Vec<u8>>,
870    pub immediate: bool,
871    pub policy: ResourcePolicy,
872}
873
874impl TimerResource {
875    pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
876        Self {
877            key,
878            interval_ms: interval.as_millis() as u64,
879            payload: serde_json::to_vec(&payload)
880                .expect("timer resource payload serialization must succeed"),
881            on_tick: None,
882            deps: None,
883            immediate: false,
884            policy: ResourcePolicy::RestartOnChange,
885        }
886    }
887
888    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
889        self.deps =
890            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
891        self
892    }
893
894    pub fn preserve_on_change(mut self) -> Self {
895        self.policy = ResourcePolicy::PreserveOnChange;
896        self
897    }
898
899    pub fn restart_on_change(mut self) -> Self {
900        self.policy = ResourcePolicy::RestartOnChange;
901        self
902    }
903
904    pub fn immediate(mut self) -> Self {
905        self.immediate = true;
906        self
907    }
908
909    pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
910        self.on_tick = Some(action);
911        self
912    }
913}
914
915#[derive(Default)]
916pub struct ResourceRegistry {
917    declarations: Vec<RuntimeResourceDeclaration>,
918    seen_keys: HashMap<String, usize>,
919}
920
921impl ResourceRegistry {
922    pub fn new() -> Self {
923        Self::default()
924    }
925
926    pub fn job(&mut self, resource: JobResource) {
927        self.push(RuntimeResourceDeclaration {
928            key: resource.key.as_str().to_string(),
929            deps: resource.deps.clone(),
930            policy: resource.policy,
931            kind: RuntimeResourceKind::Job(resource),
932        });
933    }
934
935    pub fn service(&mut self, resource: ServiceResource) {
936        self.push(RuntimeResourceDeclaration {
937            key: resource.key.as_str().to_string(),
938            deps: resource.deps.clone(),
939            policy: resource.policy,
940            kind: RuntimeResourceKind::Service(resource),
941        });
942    }
943
944    pub fn timer(&mut self, resource: TimerResource) {
945        self.push(RuntimeResourceDeclaration {
946            key: resource.key.as_str().to_string(),
947            deps: resource.deps.clone(),
948            policy: resource.policy,
949            kind: RuntimeResourceKind::Timer(resource),
950        });
951    }
952
953    pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
954        self.seen_keys.clear();
955        std::mem::take(&mut self.declarations)
956    }
957
958    fn push(&mut self, declaration: RuntimeResourceDeclaration) {
959        if let Some(index) = self.seen_keys.get(&declaration.key) {
960            panic!(
961                "duplicate runtime resource declaration for key '{}' at index {}",
962                declaration.key, index
963            );
964        }
965        let index = self.declarations.len();
966        self.seen_keys.insert(declaration.key.clone(), index);
967        self.declarations.push(declaration);
968    }
969}