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