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::Widget,
12    Action, ActionEnvelope, ActionId, BoxedReducer, GlobalState,
13};
14use anyhow::{anyhow, Result};
15use fission_ir::WidgetId;
16use serde::Serialize;
17use std::any::TypeId;
18use std::collections::{BTreeMap, HashMap};
19
20/// The canonical 3-argument handler signature for modern reducers.
21///
22/// ```rust,ignore
23/// fn handle_increment(state: &mut Counter, _: Increment, _ctx: &mut ReducerContext<Counter>) {
24///     state.count += 1;
25/// }
26/// ```
27pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
28
29/// Trait that allows both 2-argument (legacy) and 3-argument (modern) handler
30/// functions to be used with [`ActionRegistry::register`] and
31/// [`BuildCtxHandle::bind`](crate::build::BuildCtxHandle::bind).
32pub trait IntoHandler<S: GlobalState, A> {
33    /// Invoke the handler with the given state, action, and context.
34    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
35}
36
37// Impl for Legacy (2-arg)
38impl<S: GlobalState, A> IntoHandler<S, A> for fn(&mut S, A) {
39    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, _ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
40        (self)(state, action);
41    }
42}
43
44// Impl for Modern (3-arg)
45impl<S: GlobalState, A> IntoHandler<S, A>
46    for for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>)
47{
48    fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
49        (self)(state, action, ctx);
50    }
51}
52
53// Internal typed reducer storage
54type TypedReducer<S> = Box<
55    dyn for<'a, 'b, 'c> Fn(
56            &mut S,
57            &ActionEnvelope,
58            WidgetId,
59            &mut Effects<'a, S>,
60            &'b ActionInput,
61        ) -> Result<()>
62        + Send
63        + Sync,
64>;
65
66/// A per-frame collection of action handlers registered during widget building.
67///
68/// `ActionRegistry` is populated by [`BuildCtxHandle::bind`](crate::build::BuildCtxHandle::bind)
69/// calls. After the widget
70/// tree is built, the registry is absorbed into the [`Runtime`](crate::Runtime)
71/// via [`Runtime::absorb_registry`](crate::Runtime::absorb_registry).
72pub struct ActionRegistry<S: GlobalState> {
73    handlers: BTreeMap<ActionId, Vec<TypedReducer<S>>>,
74    runtime_handlers: BTreeMap<ActionId, Vec<BoxedReducer>>,
75}
76
77impl<S: GlobalState> Default for ActionRegistry<S> {
78    fn default() -> Self {
79        Self {
80            handlers: BTreeMap::new(),
81            runtime_handlers: BTreeMap::new(),
82        }
83    }
84}
85
86impl<S: GlobalState> ActionRegistry<S> {
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    pub fn register<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
92        &mut self,
93        handler: H,
94    ) {
95        let action_id = A::static_id();
96
97        let typed_reducer: TypedReducer<S> = Box::new(
98            move |state: &mut S,
99                  envelope: &ActionEnvelope,
100                  _target,
101                  effects,
102                  input|
103                  -> Result<()> {
104                let action: A = serde_json::from_slice(&envelope.payload)
105                    .map_err(|e| anyhow!("Failed to deserialize action: {}", e))?;
106
107                let mut ctx = ReducerContext { effects, input };
108
109                handler.call(state, action, &mut ctx);
110                Ok(())
111            },
112        );
113
114        self.handlers
115            .entry(action_id)
116            .or_default()
117            .push(typed_reducer);
118    }
119
120    pub fn action_ids(&self) -> Vec<ActionId> {
121        self.handlers
122            .keys()
123            .chain(self.runtime_handlers.keys())
124            .copied()
125            .collect()
126    }
127
128    pub(crate) fn register_runtime_reducer(&mut self, action_id: ActionId, reducer: BoxedReducer) {
129        self.runtime_handlers
130            .entry(action_id)
131            .or_default()
132            .push(reducer);
133    }
134
135    pub fn dispatch_with_input(
136        &mut self,
137        state: &mut S,
138        action: &ActionEnvelope,
139        target: WidgetId,
140        input: &ActionInput,
141    ) -> Result<Vec<EffectEnvelope>> {
142        let mut effects_builder = Effects::new_headless(0);
143        let target: WidgetId = target.into();
144        if let Some(reducers) = self.handlers.get_mut(&action.id) {
145            for reducer in reducers {
146                reducer(state, action, target, &mut effects_builder, input)?;
147            }
148        }
149        Ok(effects_builder.out)
150    }
151
152    pub fn dispatch(
153        &mut self,
154        state: &mut S,
155        action: &ActionEnvelope,
156        target: WidgetId,
157    ) -> Result<Vec<EffectEnvelope>> {
158        self.dispatch_with_input(state, action, target, &ActionInput::None)
159    }
160
161    pub(crate) fn into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
162        let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
163        let state_type_id = TypeId::of::<S>();
164
165        for (action_id, mut reducers) in self.runtime_handlers {
166            runtime_reducers
167                .entry(action_id)
168                .or_default()
169                .append(&mut reducers);
170        }
171
172        for (action_id, typed_reducers) in self.handlers {
173            for typed_reducer in typed_reducers {
174                let boxed_reducer: BoxedReducer = Box::new(
175                    move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
176                          action: &ActionEnvelope,
177                          target: WidgetId,
178                          out_effects: &mut Vec<EffectEnvelope>,
179                          input: &ActionInput|
180                          -> Result<()> {
181                        if let Some(state_box) = app_states.get_mut(&state_type_id) {
182                            let concrete_state =
183                                state_box.downcast_mut::<S>().ok_or_else(|| {
184                                    anyhow!("Failed to downcast GlobalState to concrete type")
185                                })?;
186
187                            let mut effects_builder = Effects::new_headless(0);
188
189                            typed_reducer(
190                                concrete_state,
191                                action,
192                                target,
193                                &mut effects_builder,
194                                input,
195                            )?;
196
197                            out_effects.extend(effects_builder.out);
198
199                            Ok(())
200                        } else {
201                            anyhow::bail!("Target GlobalState for reducer not found in runtime.");
202                        }
203                    },
204                );
205
206                runtime_reducers
207                    .entry(action_id)
208                    .or_default()
209                    .push(boxed_reducer);
210            }
211        }
212        runtime_reducers
213    }
214}
215
216/// Registration data for a [`Video`](crate::ui::Video) widget collected during
217/// widget building.
218#[derive(Clone, Debug)]
219pub struct VideoRegistration {
220    /// The stable widget identity of the video node.
221    pub node_id: WidgetId,
222    /// URL or asset path to the video file.
223    pub source: String,
224    /// Whether to start playing automatically.
225    pub autoplay: bool,
226    /// Whether to loop playback.
227    pub loop_playback: bool,
228}
229
230/// Registration data for a platform web view collected during widget building.
231#[derive(Clone, Debug)]
232pub struct WebRegistration {
233    /// The stable widget identity of the web view node.
234    pub node_id: WidgetId,
235    /// The URL to load.
236    pub url: String,
237    /// Optional custom user-agent string.
238    pub user_agent: Option<String>,
239}
240
241/// Z-order layer for portal entries.
242///
243/// Portals are sorted by layer (then by registration order within a layer).
244/// Higher layers paint on top of lower layers.
245#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
246pub enum PortalLayer {
247    /// Default overlay layer.
248    Default = 0,
249    /// Modal dialog layer.
250    Modal = 100,
251    /// Flyout / dropdown layer.
252    Flyout = 200,
253    /// Toast notification layer (topmost).
254    Toast = 300,
255}
256
257/// An entry in the portal overlay stack.
258///
259/// Created by [`crate::internal::BuildCtx::register_portal`] and friends.
260#[derive(Clone, Debug)]
261pub struct PortalEntry {
262    /// Which overlay layer this portal belongs to.
263    pub layer: PortalLayer,
264    /// Insertion order (for stable ordering within a layer).
265    pub seq: u64,
266    /// Optional stable identity.
267    pub id: Option<WidgetId>,
268    /// The portal's widget tree.
269    pub node: Widget,
270}
271
272/// The mutable context available during `impl From<Component> for Widget`.
273///
274#[derive(Clone, Copy)]
275pub struct VideoControlCtx {
276    pub(crate) target: WidgetId,
277}
278
279impl VideoControlCtx {
280    pub fn play(&self) -> ActionEnvelope {
281        let action = VideoPlay {
282            target: self.target,
283        };
284        ActionEnvelope {
285            id: VideoPlay::static_id(),
286            payload: action.encode(),
287        }
288    }
289
290    pub fn pause(&self) -> ActionEnvelope {
291        let action = VideoPause {
292            target: self.target,
293        };
294        ActionEnvelope {
295            id: VideoPause::static_id(),
296            payload: action.encode(),
297        }
298    }
299
300    pub fn stop(&self) -> ActionEnvelope {
301        let action = VideoStop {
302            target: self.target,
303        };
304        ActionEnvelope {
305            id: VideoStop::static_id(),
306            payload: action.encode(),
307        }
308    }
309
310    pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
311        let action = VideoSeek {
312            target: self.target,
313            position_ms,
314        };
315        ActionEnvelope {
316            id: VideoSeek::static_id(),
317            payload: action.encode(),
318        }
319    }
320
321    pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
322        let action = VideoSetRate {
323            target: self.target,
324            rate,
325        };
326        ActionEnvelope {
327            id: VideoSetRate::static_id(),
328            payload: action.encode(),
329        }
330    }
331
332    pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
333        let action = VideoSetVolume {
334            target: self.target,
335            volume,
336        };
337        ActionEnvelope {
338            id: VideoSetVolume::static_id(),
339            payload: action.encode(),
340        }
341    }
342
343    pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
344        let action = VideoSetMuted {
345            target: self.target,
346            muted,
347        };
348        ActionEnvelope {
349            id: VideoSetMuted::static_id(),
350            payload: action.encode(),
351        }
352    }
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Hash)]
356pub struct ResourceKey(String);
357
358impl ResourceKey {
359    pub fn new(name: impl Into<String>) -> Self {
360        Self(name.into())
361    }
362
363    pub fn widget(name: impl AsRef<str>, id: WidgetId) -> Self {
364        Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
365    }
366
367    pub fn as_str(&self) -> &str {
368        &self.0
369    }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373pub enum ResourcePolicy {
374    PreserveOnChange,
375    RestartOnChange,
376}
377
378impl Default for ResourcePolicy {
379    fn default() -> Self {
380        Self::RestartOnChange
381    }
382}
383
384#[derive(Clone, Debug, PartialEq)]
385pub struct RuntimeResourceDeclaration {
386    pub key: String,
387    pub deps: Option<Vec<u8>>,
388    pub policy: ResourcePolicy,
389    pub kind: RuntimeResourceKind,
390}
391
392#[derive(Clone, Debug, PartialEq)]
393pub enum RuntimeResourceKind {
394    Job(JobResource),
395    Service(ServiceResource),
396    Timer(TimerResource),
397}
398
399#[derive(Clone, Debug, PartialEq)]
400pub struct JobResource {
401    pub key: ResourceKey,
402    pub effect: EffectEnvelope,
403    pub deps: Option<Vec<u8>>,
404    pub policy: ResourcePolicy,
405}
406
407impl JobResource {
408    pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
409        let payload =
410            serde_json::to_vec(&request).expect("job resource request serialization must succeed");
411        Self {
412            key,
413            effect: EffectEnvelope {
414                req_id: 0,
415                effect: Effect::Job(JobRequestPayload {
416                    job_name: job.name.to_string(),
417                    payload,
418                }),
419                on_ok: None,
420                on_err: None,
421                service_bindings: None,
422                resource: None,
423            },
424            deps: None,
425            policy: ResourcePolicy::RestartOnChange,
426        }
427    }
428
429    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
430        self.deps =
431            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
432        self
433    }
434
435    pub fn preserve_on_change(mut self) -> Self {
436        self.policy = ResourcePolicy::PreserveOnChange;
437        self
438    }
439
440    pub fn restart_on_change(mut self) -> Self {
441        self.policy = ResourcePolicy::RestartOnChange;
442        self
443    }
444
445    pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
446        self.effect.on_ok = Some(action);
447        self
448    }
449
450    pub fn on_err(mut self, action: ActionEnvelope) -> Self {
451        self.effect.on_err = Some(action);
452        self
453    }
454}
455
456#[derive(Clone, Debug, PartialEq)]
457pub struct ServiceResource {
458    pub key: ResourceKey,
459    pub effect: EffectEnvelope,
460    pub deps: Option<Vec<u8>>,
461    pub policy: ResourcePolicy,
462}
463
464impl ServiceResource {
465    pub fn new<Svc: ServiceSpec>(
466        key: ResourceKey,
467        slot: ServiceSlot<Svc>,
468        config: Svc::Config,
469    ) -> Self {
470        let config = serde_json::to_vec(&config)
471            .expect("service resource config serialization must succeed");
472        Self {
473            key,
474            effect: EffectEnvelope {
475                req_id: 0,
476                effect: Effect::StartService(ServiceStartPayload {
477                    service_name: slot.ty.name.to_string(),
478                    slot_key: slot.slot_key().to_string(),
479                    config,
480                }),
481                on_ok: None,
482                on_err: None,
483                service_bindings: Some(ServiceBindings::default()),
484                resource: None,
485            },
486            deps: None,
487            policy: ResourcePolicy::RestartOnChange,
488        }
489    }
490
491    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
492        self.deps =
493            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
494        self
495    }
496
497    pub fn preserve_on_change(mut self) -> Self {
498        self.policy = ResourcePolicy::PreserveOnChange;
499        self
500    }
501
502    pub fn restart_on_change(mut self) -> Self {
503        self.policy = ResourcePolicy::RestartOnChange;
504        self
505    }
506
507    pub fn on_started(mut self, action: ActionEnvelope) -> Self {
508        if let Some(bindings) = self.effect.service_bindings.as_mut() {
509            bindings.on_started = Some(action);
510        }
511        self
512    }
513
514    pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
515        if let Some(bindings) = self.effect.service_bindings.as_mut() {
516            bindings.on_start_failed = Some(action);
517        }
518        self
519    }
520
521    pub fn on_event(mut self, action: ActionEnvelope) -> Self {
522        if let Some(bindings) = self.effect.service_bindings.as_mut() {
523            bindings.on_event = Some(action);
524        }
525        self
526    }
527
528    pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
529        if let Some(bindings) = self.effect.service_bindings.as_mut() {
530            bindings.on_stopped = Some(action);
531        }
532        self
533    }
534
535    pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
536        if let Some(bindings) = self.effect.service_bindings.as_mut() {
537            bindings.on_command_ok = Some(action);
538        }
539        self
540    }
541
542    pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
543        if let Some(bindings) = self.effect.service_bindings.as_mut() {
544            bindings.on_command_err = Some(action);
545        }
546        self
547    }
548}
549
550#[derive(Clone, Debug, PartialEq, Eq)]
551pub struct TimerResource {
552    pub key: ResourceKey,
553    pub interval_ms: u64,
554    pub payload: Vec<u8>,
555    pub on_tick: Option<ActionEnvelope>,
556    pub deps: Option<Vec<u8>>,
557    pub immediate: bool,
558    pub policy: ResourcePolicy,
559}
560
561impl TimerResource {
562    pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
563        Self {
564            key,
565            interval_ms: interval.as_millis() as u64,
566            payload: serde_json::to_vec(&payload)
567                .expect("timer resource payload serialization must succeed"),
568            on_tick: None,
569            deps: None,
570            immediate: false,
571            policy: ResourcePolicy::RestartOnChange,
572        }
573    }
574
575    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
576        self.deps =
577            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
578        self
579    }
580
581    pub fn preserve_on_change(mut self) -> Self {
582        self.policy = ResourcePolicy::PreserveOnChange;
583        self
584    }
585
586    pub fn restart_on_change(mut self) -> Self {
587        self.policy = ResourcePolicy::RestartOnChange;
588        self
589    }
590
591    pub fn immediate(mut self) -> Self {
592        self.immediate = true;
593        self
594    }
595
596    pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
597        self.on_tick = Some(action);
598        self
599    }
600}
601
602#[derive(Default)]
603pub struct ResourceRegistry {
604    declarations: Vec<RuntimeResourceDeclaration>,
605    seen_keys: HashMap<String, usize>,
606}
607
608impl ResourceRegistry {
609    pub fn new() -> Self {
610        Self::default()
611    }
612
613    pub fn job(&mut self, resource: JobResource) {
614        self.push(RuntimeResourceDeclaration {
615            key: resource.key.as_str().to_string(),
616            deps: resource.deps.clone(),
617            policy: resource.policy,
618            kind: RuntimeResourceKind::Job(resource),
619        });
620    }
621
622    pub fn service(&mut self, resource: ServiceResource) {
623        self.push(RuntimeResourceDeclaration {
624            key: resource.key.as_str().to_string(),
625            deps: resource.deps.clone(),
626            policy: resource.policy,
627            kind: RuntimeResourceKind::Service(resource),
628        });
629    }
630
631    pub fn timer(&mut self, resource: TimerResource) {
632        self.push(RuntimeResourceDeclaration {
633            key: resource.key.as_str().to_string(),
634            deps: resource.deps.clone(),
635            policy: resource.policy,
636            kind: RuntimeResourceKind::Timer(resource),
637        });
638    }
639
640    pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
641        self.seen_keys.clear();
642        std::mem::take(&mut self.declarations)
643    }
644
645    fn push(&mut self, declaration: RuntimeResourceDeclaration) {
646        if let Some(index) = self.seen_keys.get(&declaration.key) {
647            panic!(
648                "duplicate runtime resource declaration for key '{}' at index {}",
649                declaration.key, index
650            );
651        }
652        let index = self.declarations.len();
653        self.seen_keys.insert(declaration.key.clone(), index);
654        self.declarations.push(declaration);
655    }
656}