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 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/// Identifies which visual property an animation targets.
254///
255/// Built-in properties have well-known default values (e.g. opacity defaults
256/// to 1.0, translation defaults to 0.0). Custom properties use 0.0.
257///
258/// # Example
259///
260/// ```rust,ignore
261/// ctx.anim_for(widget_id).request(AnimationRequest {
262///     property: AnimationPropertyId::Opacity,
263///     from: AnimationStartValue::Current,
264///     to: 0.0,
265///     duration_ms: 300,
266///     repeat: false,
267///     delay_ms: 0,
268/// });
269/// ```
270#[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/// Where an animation starts from.
309#[derive(Clone, Debug)]
310pub enum AnimationStartValue {
311    /// Start from an explicit value.
312    Explicit(f32),
313    /// Start from whatever the current animated value is.
314    Current,
315}
316
317/// A request to animate a visual property on a widget.
318///
319/// Registered via [`BuildCtx::request_animation_for`] or
320/// [`AnimCtx::request`].
321/// Easing function applied to animation progress before interpolation.
322#[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    /// Map a linear progress value `t` in [0, 1] to an eased value.
339    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                // Simplified cubic bezier approximation (y-axis only)
353                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    /// The property to animate.
363    pub property: AnimationPropertyId,
364    /// Starting value.
365    pub from: AnimationStartValue,
366    /// Target value.
367    pub to: f32,
368    /// Duration in milliseconds.
369    pub duration_ms: u64,
370    /// Whether to loop the animation.
371    pub repeat: bool,
372    /// Delay before the animation starts (in milliseconds).
373    pub delay_ms: u64,
374    /// Optional preferred redraw cadence for this animation (in milliseconds).
375    ///
376    /// This is primarily useful for low-priority repeating effects such as
377    /// loading indicators that do not need full frame-rate updates.
378    pub frame_interval_ms: Option<u64>,
379    pub easing: EasingFunction,
380}
381
382/// Registration data for a [`Video`](crate::ui::Video) widget collected during
383/// widget building.
384#[derive(Clone, Debug)]
385pub struct VideoRegistration {
386    /// The stable widget identity of the video node.
387    pub node_id: WidgetNodeId,
388    /// URL or asset path to the video file.
389    pub source: String,
390    /// Whether to start playing automatically.
391    pub autoplay: bool,
392    /// Whether to loop playback.
393    pub loop_playback: bool,
394}
395
396/// Registration data for a platform web view collected during widget building.
397#[derive(Clone, Debug)]
398pub struct WebRegistration {
399    /// The stable widget identity of the web view node.
400    pub node_id: WidgetNodeId,
401    /// The URL to load.
402    pub url: String,
403    /// Optional custom user-agent string.
404    pub user_agent: Option<String>,
405}
406
407/// The mutable context passed to [`Widget::build`](crate::Widget::build).
408///
409/// `BuildCtx` is where widgets register side-effects that must survive beyond
410/// the build phase:
411///
412/// - **Action binding** -- [`bind`](BuildCtx::bind) registers a handler and
413///   returns an [`ActionEnvelope`] that can be stored in widget fields like
414///   `on_press`.
415/// - **Portals** -- [`register_portal`](BuildCtx::register_portal) places a
416///   node in the global overlay stack (modals, toasts, flyouts).
417/// - **Animations** -- [`request_animation_for`](BuildCtx::request_animation_for)
418///   or the [`anim_for`](BuildCtx::anim_for) helper.
419/// - **Video / WebView registration** -- [`register_video`](BuildCtx::register_video),
420///   [`register_web_view`](BuildCtx::register_web_view).
421///
422/// # Example
423///
424/// ```rust,ignore
425/// fn build(&self, ctx: &mut BuildCtx<S>, view: &View<S>) -> Node {
426///     let on_press = ctx.bind(MyAction { .. }, reduce_with!(handler));
427///     Button { on_press: Some(on_press), ..Default::default() }.into_node()
428/// }
429/// ```
430pub struct BuildCtx<S: AppState> {
431    /// The action registry that accumulates handlers during the build phase.
432    pub registry: ActionRegistry<S>,
433    /// Declarative runtime resources collected during the build phase.
434    pub resources: ResourceRegistry,
435    /// Pending animation requests.
436    pub animation_requests: Vec<(WidgetNodeId, AnimationRequest)>,
437    /// Registered video nodes.
438    pub video_nodes: Vec<VideoRegistration>,
439    /// Registered web view nodes.
440    pub web_nodes: Vec<WebRegistration>,
441    /// Portal entries (overlays, modals, toasts).
442    pub portals: Vec<PortalEntry>,
443    portal_seq: u64,
444}
445
446/// Z-order layer for portal entries.
447///
448/// Portals are sorted by layer (then by registration order within a layer).
449/// Higher layers paint on top of lower layers.
450#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
451pub enum PortalLayer {
452    /// Default overlay layer.
453    Default = 0,
454    /// Modal dialog layer.
455    Modal = 100,
456    /// Flyout / dropdown layer.
457    Flyout = 200,
458    /// Toast notification layer (topmost).
459    Toast = 300,
460}
461
462/// An entry in the portal overlay stack.
463///
464/// Created by [`BuildCtx::register_portal`] and friends.
465#[derive(Clone, Debug)]
466pub struct PortalEntry {
467    /// Which overlay layer this portal belongs to.
468    pub layer: PortalLayer,
469    /// Insertion order (for stable ordering within a layer).
470    pub seq: u64,
471    /// Optional stable identity.
472    pub id: Option<WidgetNodeId>,
473    /// The portal's widget tree.
474    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}