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::{VideoAudioOptions, 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    /// Audio-session behavior requested by this video.
229    pub audio: VideoAudioOptions,
230}
231
232/// Registration data for a platform web view collected during widget building.
233#[derive(Clone, Debug)]
234pub struct WebRegistration {
235    /// The stable widget identity of the web view node.
236    pub node_id: WidgetId,
237    /// The URL to load.
238    pub url: String,
239    /// Optional custom user-agent string.
240    pub user_agent: Option<String>,
241}
242
243/// Z-order layer for portal entries.
244///
245/// Portals are sorted by layer (then by registration order within a layer).
246/// Higher layers paint on top of lower layers.
247#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
248pub enum PortalLayer {
249    /// Default overlay layer.
250    Default = 0,
251    /// Modal dialog layer.
252    Modal = 100,
253    /// Flyout / dropdown layer.
254    Flyout = 200,
255    /// Toast notification layer (topmost).
256    Toast = 300,
257}
258
259/// An entry in the portal overlay stack.
260///
261/// Created by [`crate::internal::BuildCtx::register_portal`] and friends.
262#[derive(Clone, Debug)]
263pub struct PortalEntry {
264    /// Which overlay layer this portal belongs to.
265    pub layer: PortalLayer,
266    /// Insertion order (for stable ordering within a layer).
267    pub seq: u64,
268    /// Optional stable identity.
269    pub id: Option<WidgetId>,
270    /// The portal's widget tree.
271    pub node: Widget,
272}
273
274/// The mutable context available during `impl From<Component> for Widget`.
275///
276#[derive(Clone, Copy)]
277pub struct VideoControlCtx {
278    pub(crate) target: WidgetId,
279}
280
281impl VideoControlCtx {
282    pub fn play(&self) -> ActionEnvelope {
283        let action = VideoPlay {
284            target: self.target,
285        };
286        ActionEnvelope {
287            id: VideoPlay::static_id(),
288            payload: action.encode(),
289        }
290    }
291
292    pub fn pause(&self) -> ActionEnvelope {
293        let action = VideoPause {
294            target: self.target,
295        };
296        ActionEnvelope {
297            id: VideoPause::static_id(),
298            payload: action.encode(),
299        }
300    }
301
302    pub fn stop(&self) -> ActionEnvelope {
303        let action = VideoStop {
304            target: self.target,
305        };
306        ActionEnvelope {
307            id: VideoStop::static_id(),
308            payload: action.encode(),
309        }
310    }
311
312    pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
313        let action = VideoSeek {
314            target: self.target,
315            position_ms,
316        };
317        ActionEnvelope {
318            id: VideoSeek::static_id(),
319            payload: action.encode(),
320        }
321    }
322
323    pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
324        let action = VideoSetRate {
325            target: self.target,
326            rate,
327        };
328        ActionEnvelope {
329            id: VideoSetRate::static_id(),
330            payload: action.encode(),
331        }
332    }
333
334    pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
335        let action = VideoSetVolume {
336            target: self.target,
337            volume,
338        };
339        ActionEnvelope {
340            id: VideoSetVolume::static_id(),
341            payload: action.encode(),
342        }
343    }
344
345    pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
346        let action = VideoSetMuted {
347            target: self.target,
348            muted,
349        };
350        ActionEnvelope {
351            id: VideoSetMuted::static_id(),
352            payload: action.encode(),
353        }
354    }
355}
356
357#[derive(Clone, Debug, PartialEq, Eq, Hash)]
358pub struct ResourceKey(String);
359
360impl ResourceKey {
361    pub fn new(name: impl Into<String>) -> Self {
362        Self(name.into())
363    }
364
365    pub fn widget(name: impl AsRef<str>, id: WidgetId) -> Self {
366        Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
367    }
368
369    pub fn as_str(&self) -> &str {
370        &self.0
371    }
372}
373
374#[derive(Clone, Copy, Debug, PartialEq, Eq)]
375pub enum ResourcePolicy {
376    PreserveOnChange,
377    RestartOnChange,
378}
379
380impl Default for ResourcePolicy {
381    fn default() -> Self {
382        Self::RestartOnChange
383    }
384}
385
386#[derive(Clone, Debug, PartialEq)]
387pub struct RuntimeResourceDeclaration {
388    pub key: String,
389    pub deps: Option<Vec<u8>>,
390    pub policy: ResourcePolicy,
391    pub kind: RuntimeResourceKind,
392}
393
394#[derive(Clone, Debug, PartialEq)]
395pub enum RuntimeResourceKind {
396    Job(JobResource),
397    Service(ServiceResource),
398    Timer(TimerResource),
399}
400
401#[derive(Clone, Debug, PartialEq)]
402pub struct JobResource {
403    pub key: ResourceKey,
404    pub effect: EffectEnvelope,
405    pub deps: Option<Vec<u8>>,
406    pub policy: ResourcePolicy,
407}
408
409impl JobResource {
410    pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
411        let payload =
412            serde_json::to_vec(&request).expect("job resource request serialization must succeed");
413        Self {
414            key,
415            effect: EffectEnvelope {
416                req_id: 0,
417                effect: Effect::Job(JobRequestPayload {
418                    job_name: job.name.to_string(),
419                    payload,
420                }),
421                on_ok: None,
422                on_err: None,
423                service_bindings: None,
424                resource: None,
425            },
426            deps: None,
427            policy: ResourcePolicy::RestartOnChange,
428        }
429    }
430
431    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
432        self.deps =
433            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
434        self
435    }
436
437    pub fn preserve_on_change(mut self) -> Self {
438        self.policy = ResourcePolicy::PreserveOnChange;
439        self
440    }
441
442    pub fn restart_on_change(mut self) -> Self {
443        self.policy = ResourcePolicy::RestartOnChange;
444        self
445    }
446
447    pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
448        self.effect.on_ok = Some(action);
449        self
450    }
451
452    pub fn on_err(mut self, action: ActionEnvelope) -> Self {
453        self.effect.on_err = Some(action);
454        self
455    }
456}
457
458#[derive(Clone, Debug, PartialEq)]
459pub struct ServiceResource {
460    pub key: ResourceKey,
461    pub effect: EffectEnvelope,
462    pub deps: Option<Vec<u8>>,
463    pub policy: ResourcePolicy,
464}
465
466impl ServiceResource {
467    pub fn new<Svc: ServiceSpec>(
468        key: ResourceKey,
469        slot: ServiceSlot<Svc>,
470        config: Svc::Config,
471    ) -> Self {
472        let config = serde_json::to_vec(&config)
473            .expect("service resource config serialization must succeed");
474        Self {
475            key,
476            effect: EffectEnvelope {
477                req_id: 0,
478                effect: Effect::StartService(ServiceStartPayload {
479                    service_name: slot.ty.name.to_string(),
480                    slot_key: slot.slot_key().to_string(),
481                    config,
482                }),
483                on_ok: None,
484                on_err: None,
485                service_bindings: Some(ServiceBindings::default()),
486                resource: None,
487            },
488            deps: None,
489            policy: ResourcePolicy::RestartOnChange,
490        }
491    }
492
493    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
494        self.deps =
495            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
496        self
497    }
498
499    pub fn preserve_on_change(mut self) -> Self {
500        self.policy = ResourcePolicy::PreserveOnChange;
501        self
502    }
503
504    pub fn restart_on_change(mut self) -> Self {
505        self.policy = ResourcePolicy::RestartOnChange;
506        self
507    }
508
509    pub fn on_started(mut self, action: ActionEnvelope) -> Self {
510        if let Some(bindings) = self.effect.service_bindings.as_mut() {
511            bindings.on_started = Some(action);
512        }
513        self
514    }
515
516    pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
517        if let Some(bindings) = self.effect.service_bindings.as_mut() {
518            bindings.on_start_failed = Some(action);
519        }
520        self
521    }
522
523    pub fn on_event(mut self, action: ActionEnvelope) -> Self {
524        if let Some(bindings) = self.effect.service_bindings.as_mut() {
525            bindings.on_event = Some(action);
526        }
527        self
528    }
529
530    pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
531        if let Some(bindings) = self.effect.service_bindings.as_mut() {
532            bindings.on_stopped = Some(action);
533        }
534        self
535    }
536
537    pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
538        if let Some(bindings) = self.effect.service_bindings.as_mut() {
539            bindings.on_command_ok = Some(action);
540        }
541        self
542    }
543
544    pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
545        if let Some(bindings) = self.effect.service_bindings.as_mut() {
546            bindings.on_command_err = Some(action);
547        }
548        self
549    }
550}
551
552#[derive(Clone, Debug, PartialEq, Eq)]
553pub struct TimerResource {
554    pub key: ResourceKey,
555    pub interval_ms: u64,
556    pub payload: Vec<u8>,
557    pub on_tick: Option<ActionEnvelope>,
558    pub deps: Option<Vec<u8>>,
559    pub immediate: bool,
560    pub policy: ResourcePolicy,
561}
562
563impl TimerResource {
564    pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
565        Self {
566            key,
567            interval_ms: interval.as_millis() as u64,
568            payload: serde_json::to_vec(&payload)
569                .expect("timer resource payload serialization must succeed"),
570            on_tick: None,
571            deps: None,
572            immediate: false,
573            policy: ResourcePolicy::RestartOnChange,
574        }
575    }
576
577    pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
578        self.deps =
579            Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
580        self
581    }
582
583    pub fn preserve_on_change(mut self) -> Self {
584        self.policy = ResourcePolicy::PreserveOnChange;
585        self
586    }
587
588    pub fn restart_on_change(mut self) -> Self {
589        self.policy = ResourcePolicy::RestartOnChange;
590        self
591    }
592
593    pub fn immediate(mut self) -> Self {
594        self.immediate = true;
595        self
596    }
597
598    pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
599        self.on_tick = Some(action);
600        self
601    }
602}
603
604#[derive(Default)]
605pub struct ResourceRegistry {
606    declarations: Vec<RuntimeResourceDeclaration>,
607    seen_keys: HashMap<String, usize>,
608}
609
610impl ResourceRegistry {
611    pub fn new() -> Self {
612        Self::default()
613    }
614
615    pub fn job(&mut self, resource: JobResource) {
616        self.push(RuntimeResourceDeclaration {
617            key: resource.key.as_str().to_string(),
618            deps: resource.deps.clone(),
619            policy: resource.policy,
620            kind: RuntimeResourceKind::Job(resource),
621        });
622    }
623
624    pub fn service(&mut self, resource: ServiceResource) {
625        self.push(RuntimeResourceDeclaration {
626            key: resource.key.as_str().to_string(),
627            deps: resource.deps.clone(),
628            policy: resource.policy,
629            kind: RuntimeResourceKind::Service(resource),
630        });
631    }
632
633    pub fn timer(&mut self, resource: TimerResource) {
634        self.push(RuntimeResourceDeclaration {
635            key: resource.key.as_str().to_string(),
636            deps: resource.deps.clone(),
637            policy: resource.policy,
638            kind: RuntimeResourceKind::Timer(resource),
639        });
640    }
641
642    pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
643        self.seen_keys.clear();
644        std::mem::take(&mut self.declarations)
645    }
646
647    fn push(&mut self, declaration: RuntimeResourceDeclaration) {
648        if let Some(index) = self.seen_keys.get(&declaration.key) {
649            panic!(
650                "duplicate runtime resource declaration for key '{}' at index {}",
651                declaration.key, index
652            );
653        }
654        let index = self.declarations.len();
655        self.seen_keys.insert(declaration.key.clone(), index);
656        self.declarations.push(declaration);
657    }
658}