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_diagnostics::prelude as diag;
16use fission_ir::WidgetId;
17use serde::Serialize;
18use std::any::TypeId;
19use std::collections::{BTreeMap, BTreeSet, HashMap};
20use std::error::Error;
21use std::fmt;
22
23#[derive(Debug)]
24pub(crate) struct ActionDeserializationError;
25
26impl ActionDeserializationError {
27 pub(crate) fn new(_source: serde_json::Error) -> Self {
28 Self
29 }
30}
31
32impl fmt::Display for ActionDeserializationError {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 formatter.write_str("failed to deserialize action payload")
35 }
36}
37
38impl Error for ActionDeserializationError {}
39
40pub(crate) fn emit_action_dispatch_failure(
41 action_id: ActionId,
42 target: WidgetId,
43 error: &anyhow::Error,
44) {
45 let failure_kind = if error.downcast_ref::<ActionDeserializationError>().is_some() {
46 "action_deserialization"
47 } else {
48 "reducer_dispatch"
49 };
50 #[cfg(not(target_arch = "wasm32"))]
51 eprintln!(
52 "{}",
53 action_dispatch_failure_message(action_id, target, failure_kind)
54 );
55 diag::emit(
56 diag::DiagCategory::Input,
57 diag::DiagLevel::Error,
58 diag::DiagEventKind::ActionDispatchFailed {
59 action_id: action_id.as_u128(),
60 target: target.as_u128(),
61 failure_kind: failure_kind.into(),
62 },
63 );
64}
65
66#[cfg(any(not(target_arch = "wasm32"), test))]
67fn action_dispatch_failure_message(
68 action_id: ActionId,
69 target: WidgetId,
70 failure_kind: &'static str,
71) -> String {
72 format!(
73 "Fission action dispatch failed: action_id={} target={} failure_kind={failure_kind}",
74 action_id.as_u128(),
75 target.as_u128()
76 )
77}
78
79pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
87
88pub trait IntoHandler<S: GlobalState, A> {
92 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
94}
95
96impl<S: GlobalState, A> IntoHandler<S, A> for fn(&mut S, A) {
98 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, _ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
99 (self)(state, action);
100 }
101}
102
103impl<S: GlobalState, A> IntoHandler<S, A>
105 for for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>)
106{
107 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>) {
108 (self)(state, action, ctx);
109 }
110}
111
112type TypedReducer<S> = Box<
114 dyn for<'a, 'b, 'c> Fn(
115 &mut S,
116 &ActionEnvelope,
117 WidgetId,
118 &mut Effects<'a, S>,
119 &'b ActionInput,
120 ) -> Result<()>
121 + Send
122 + Sync,
123>;
124
125fn into_boxed_runtime_reducer<S: GlobalState>(
126 state_type_id: TypeId,
127 typed_reducer: TypedReducer<S>,
128) -> BoxedReducer {
129 Box::new(
130 move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
131 action: &ActionEnvelope,
132 target: WidgetId,
133 out_effects: &mut Vec<EffectEnvelope>,
134 input: &ActionInput,
135 callback_registry|
136 -> Result<()> {
137 if let Some(state_box) = app_states.get_mut(&state_type_id) {
138 let concrete_state = state_box
139 .downcast_mut::<S>()
140 .ok_or_else(|| anyhow!("Failed to downcast GlobalState to concrete type"))?;
141
142 let mut effects_builder = Effects::new_runtime(0, callback_registry.clone());
143
144 typed_reducer(concrete_state, action, target, &mut effects_builder, input)?;
145
146 out_effects.extend(effects_builder.out);
147
148 Ok(())
149 } else {
150 anyhow::bail!("Target GlobalState for reducer not found in runtime.");
151 }
152 },
153 )
154}
155
156pub struct ActionRegistry<S: GlobalState> {
163 handlers: BTreeMap<ActionId, Vec<TypedReducer<S>>>,
164 bound_handlers: BTreeMap<ActionId, TypedReducer<S>>,
165 runtime_handlers: BTreeMap<ActionId, BoxedReducer>,
166}
167
168impl<S: GlobalState> Default for ActionRegistry<S> {
169 fn default() -> Self {
170 Self {
171 handlers: BTreeMap::new(),
172 bound_handlers: BTreeMap::new(),
173 runtime_handlers: BTreeMap::new(),
174 }
175 }
176}
177
178impl<S: GlobalState> ActionRegistry<S> {
179 pub fn new() -> Self {
180 Self::default()
181 }
182
183 pub fn register<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
189 &mut self,
190 handler: H,
191 ) {
192 self.register_with_id(A::static_id(), handler);
193 }
194
195 pub(crate) fn register_with_id<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
196 &mut self,
197 action_id: ActionId,
198 handler: H,
199 ) {
200 self.handlers
201 .entry(action_id)
202 .or_default()
203 .push(Self::typed_reducer(handler));
204 }
205
206 pub(crate) fn register_bound<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
207 &mut self,
208 handler: H,
209 ) {
210 self.bound_handlers
211 .entry(A::static_id())
212 .or_insert_with(|| Self::typed_reducer(handler));
213 }
214
215 fn typed_reducer<A: Action, H: IntoHandler<S, A> + Send + Sync + 'static>(
216 handler: H,
217 ) -> TypedReducer<S> {
218 Box::new(
219 move |state: &mut S,
220 envelope: &ActionEnvelope,
221 _target,
222 effects,
223 input|
224 -> Result<()> {
225 let action: A = serde_json::from_slice(&envelope.payload)
226 .map_err(ActionDeserializationError::new)?;
227
228 let mut ctx = ReducerContext { effects, input };
229
230 handler.call(state, action, &mut ctx);
231 Ok(())
232 },
233 )
234 }
235
236 pub fn action_ids(&self) -> Vec<ActionId> {
237 self.handlers
238 .keys()
239 .chain(self.bound_handlers.keys())
240 .chain(self.runtime_handlers.keys())
241 .copied()
242 .collect::<BTreeSet<_>>()
243 .into_iter()
244 .collect()
245 }
246
247 pub(crate) fn register_runtime_reducer(&mut self, action_id: ActionId, reducer: BoxedReducer) {
248 self.runtime_handlers.entry(action_id).or_insert(reducer);
249 }
250
251 pub fn dispatch_with_input(
252 &mut self,
253 state: &mut S,
254 action: &ActionEnvelope,
255 target: WidgetId,
256 input: &ActionInput,
257 ) -> Result<Vec<EffectEnvelope>> {
258 let mut effects_builder = Effects::new_headless(0);
259 let target: WidgetId = target.into();
260 let dispatch_result = (|| {
261 if let Some(reducers) = self.handlers.get_mut(&action.id) {
262 reducers.iter_mut().try_for_each(|reducer| {
263 reducer(state, action, target, &mut effects_builder, input)
264 })?;
265 }
266 if let Some(reducer) = self.bound_handlers.get_mut(&action.id) {
267 reducer(state, action, target, &mut effects_builder, input)?;
268 }
269 Ok(())
270 })();
271 if let Err(error) = dispatch_result {
272 emit_action_dispatch_failure(action.id, target, &error);
273 return Err(error);
274 }
275 Ok(effects_builder.out)
276 }
277
278 pub fn dispatch(
279 &mut self,
280 state: &mut S,
281 action: &ActionEnvelope,
282 target: WidgetId,
283 ) -> Result<Vec<EffectEnvelope>> {
284 self.dispatch_with_input(state, action, target, &ActionInput::None)
285 }
286
287 pub(crate) fn into_runtime_reducers(self) -> HashMap<ActionId, Vec<BoxedReducer>> {
288 let mut runtime_reducers: HashMap<ActionId, Vec<BoxedReducer>> = HashMap::new();
289 let state_type_id = TypeId::of::<S>();
290
291 for (action_id, reducer) in self.runtime_handlers {
292 runtime_reducers.entry(action_id).or_default().push(reducer);
293 }
294
295 for (action_id, typed_reducers) in self.handlers {
296 for typed_reducer in typed_reducers {
297 runtime_reducers
298 .entry(action_id)
299 .or_default()
300 .push(into_boxed_runtime_reducer(state_type_id, typed_reducer));
301 }
302 }
303
304 for (action_id, typed_reducer) in self.bound_handlers {
305 runtime_reducers
306 .entry(action_id)
307 .or_default()
308 .push(into_boxed_runtime_reducer(state_type_id, typed_reducer));
309 }
310 runtime_reducers
311 }
312}
313
314#[derive(Clone, Debug)]
317pub struct VideoRegistration {
318 pub node_id: WidgetId,
320 pub source: String,
322 pub autoplay: bool,
324 pub loop_playback: bool,
326 pub audio: VideoAudioOptions,
328}
329
330#[derive(Clone, Debug)]
332pub struct WebRegistration {
333 pub node_id: WidgetId,
335 pub url: String,
337 pub user_agent: Option<String>,
339}
340
341#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
346pub enum PortalLayer {
347 Default = 0,
349 Modal = 100,
351 Flyout = 200,
353 Toast = 300,
355}
356
357#[derive(Clone, Debug)]
361pub struct PortalEntry {
362 pub layer: PortalLayer,
364 pub seq: u64,
366 pub id: Option<WidgetId>,
368 pub node: Widget,
370}
371
372#[derive(Clone, Copy)]
375pub struct VideoControlCtx {
376 pub(crate) target: WidgetId,
377}
378
379impl VideoControlCtx {
380 pub fn play(&self) -> ActionEnvelope {
381 let action = VideoPlay {
382 target: self.target,
383 };
384 ActionEnvelope {
385 id: VideoPlay::static_id(),
386 payload: action.encode(),
387 }
388 }
389
390 pub fn pause(&self) -> ActionEnvelope {
391 let action = VideoPause {
392 target: self.target,
393 };
394 ActionEnvelope {
395 id: VideoPause::static_id(),
396 payload: action.encode(),
397 }
398 }
399
400 pub fn stop(&self) -> ActionEnvelope {
401 let action = VideoStop {
402 target: self.target,
403 };
404 ActionEnvelope {
405 id: VideoStop::static_id(),
406 payload: action.encode(),
407 }
408 }
409
410 pub fn seek_to(&self, position_ms: u64) -> ActionEnvelope {
411 let action = VideoSeek {
412 target: self.target,
413 position_ms,
414 };
415 ActionEnvelope {
416 id: VideoSeek::static_id(),
417 payload: action.encode(),
418 }
419 }
420
421 pub fn set_rate(&self, rate: f32) -> ActionEnvelope {
422 let action = VideoSetRate {
423 target: self.target,
424 rate,
425 };
426 ActionEnvelope {
427 id: VideoSetRate::static_id(),
428 payload: action.encode(),
429 }
430 }
431
432 pub fn set_volume(&self, volume: f32) -> ActionEnvelope {
433 let action = VideoSetVolume {
434 target: self.target,
435 volume,
436 };
437 ActionEnvelope {
438 id: VideoSetVolume::static_id(),
439 payload: action.encode(),
440 }
441 }
442
443 pub fn set_muted(&self, muted: bool) -> ActionEnvelope {
444 let action = VideoSetMuted {
445 target: self.target,
446 muted,
447 };
448 ActionEnvelope {
449 id: VideoSetMuted::static_id(),
450 payload: action.encode(),
451 }
452 }
453}
454
455#[derive(Clone, Debug, PartialEq, Eq, Hash)]
456pub struct ResourceKey(String);
457
458impl ResourceKey {
459 pub fn new(name: impl Into<String>) -> Self {
460 Self(name.into())
461 }
462
463 pub fn widget(name: impl AsRef<str>, id: WidgetId) -> Self {
464 Self(format!("widget:{}:{}", id.as_u128(), name.as_ref()))
465 }
466
467 pub fn as_str(&self) -> &str {
468 &self.0
469 }
470}
471
472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
473pub enum ResourcePolicy {
474 PreserveOnChange,
475 RestartOnChange,
476}
477
478impl Default for ResourcePolicy {
479 fn default() -> Self {
480 Self::RestartOnChange
481 }
482}
483
484#[derive(Clone, Debug, PartialEq)]
485pub struct RuntimeResourceDeclaration {
486 pub key: String,
487 pub deps: Option<Vec<u8>>,
488 pub policy: ResourcePolicy,
489 pub kind: RuntimeResourceKind,
490}
491
492#[derive(Clone, Debug, PartialEq)]
493pub enum RuntimeResourceKind {
494 Job(JobResource),
495 Service(ServiceResource),
496 Timer(TimerResource),
497}
498
499#[derive(Clone, Debug, PartialEq)]
500pub struct JobResource {
501 pub key: ResourceKey,
502 pub effect: EffectEnvelope,
503 pub deps: Option<Vec<u8>>,
504 pub policy: ResourcePolicy,
505}
506
507impl JobResource {
508 pub fn new<J: JobSpec>(key: ResourceKey, job: JobRef<J>, request: J::Request) -> Self {
509 let payload =
510 serde_json::to_vec(&request).expect("job resource request serialization must succeed");
511 Self {
512 key,
513 effect: EffectEnvelope {
514 req_id: 0,
515 effect: Effect::Job(JobRequestPayload {
516 job_name: job.name.to_string(),
517 payload,
518 }),
519 on_ok: None,
520 on_err: None,
521 service_bindings: None,
522 resource: None,
523 },
524 deps: None,
525 policy: ResourcePolicy::RestartOnChange,
526 }
527 }
528
529 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
530 self.deps =
531 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
532 self
533 }
534
535 pub fn preserve_on_change(mut self) -> Self {
536 self.policy = ResourcePolicy::PreserveOnChange;
537 self
538 }
539
540 pub fn restart_on_change(mut self) -> Self {
541 self.policy = ResourcePolicy::RestartOnChange;
542 self
543 }
544
545 pub fn on_ok(mut self, action: ActionEnvelope) -> Self {
546 self.effect.on_ok = Some(action);
547 self
548 }
549
550 pub fn on_err(mut self, action: ActionEnvelope) -> Self {
551 self.effect.on_err = Some(action);
552 self
553 }
554}
555
556#[derive(Clone, Debug, PartialEq)]
557pub struct ServiceResource {
558 pub key: ResourceKey,
559 pub effect: EffectEnvelope,
560 pub deps: Option<Vec<u8>>,
561 pub policy: ResourcePolicy,
562}
563
564impl ServiceResource {
565 pub fn new<Svc: ServiceSpec>(
566 key: ResourceKey,
567 slot: ServiceSlot<Svc>,
568 config: Svc::Config,
569 ) -> Self {
570 let config = serde_json::to_vec(&config)
571 .expect("service resource config serialization must succeed");
572 Self {
573 key,
574 effect: EffectEnvelope {
575 req_id: 0,
576 effect: Effect::StartService(ServiceStartPayload {
577 service_name: slot.ty.name.to_string(),
578 slot_key: slot.slot_key().to_string(),
579 config,
580 }),
581 on_ok: None,
582 on_err: None,
583 service_bindings: Some(ServiceBindings::default()),
584 resource: None,
585 },
586 deps: None,
587 policy: ResourcePolicy::RestartOnChange,
588 }
589 }
590
591 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
592 self.deps =
593 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
594 self
595 }
596
597 pub fn preserve_on_change(mut self) -> Self {
598 self.policy = ResourcePolicy::PreserveOnChange;
599 self
600 }
601
602 pub fn restart_on_change(mut self) -> Self {
603 self.policy = ResourcePolicy::RestartOnChange;
604 self
605 }
606
607 pub fn on_started(mut self, action: ActionEnvelope) -> Self {
608 if let Some(bindings) = self.effect.service_bindings.as_mut() {
609 bindings.on_started = Some(action);
610 }
611 self
612 }
613
614 pub fn on_start_failed(mut self, action: ActionEnvelope) -> Self {
615 if let Some(bindings) = self.effect.service_bindings.as_mut() {
616 bindings.on_start_failed = Some(action);
617 }
618 self
619 }
620
621 pub fn on_event(mut self, action: ActionEnvelope) -> Self {
622 if let Some(bindings) = self.effect.service_bindings.as_mut() {
623 bindings.on_event = Some(action);
624 }
625 self
626 }
627
628 pub fn on_stopped(mut self, action: ActionEnvelope) -> Self {
629 if let Some(bindings) = self.effect.service_bindings.as_mut() {
630 bindings.on_stopped = Some(action);
631 }
632 self
633 }
634
635 pub fn on_command_ok(mut self, action: ActionEnvelope) -> Self {
636 if let Some(bindings) = self.effect.service_bindings.as_mut() {
637 bindings.on_command_ok = Some(action);
638 }
639 self
640 }
641
642 pub fn on_command_err(mut self, action: ActionEnvelope) -> Self {
643 if let Some(bindings) = self.effect.service_bindings.as_mut() {
644 bindings.on_command_err = Some(action);
645 }
646 self
647 }
648}
649
650#[derive(Clone, Debug, PartialEq, Eq)]
651pub struct TimerResource {
652 pub key: ResourceKey,
653 pub interval_ms: u64,
654 pub payload: Vec<u8>,
655 pub on_tick: Option<ActionEnvelope>,
656 pub deps: Option<Vec<u8>>,
657 pub immediate: bool,
658 pub policy: ResourcePolicy,
659}
660
661impl TimerResource {
662 pub fn new<T: Serialize>(key: ResourceKey, interval: std::time::Duration, payload: T) -> Self {
663 Self {
664 key,
665 interval_ms: interval.as_millis() as u64,
666 payload: serde_json::to_vec(&payload)
667 .expect("timer resource payload serialization must succeed"),
668 on_tick: None,
669 deps: None,
670 immediate: false,
671 policy: ResourcePolicy::RestartOnChange,
672 }
673 }
674
675 pub fn deps<T: Serialize>(mut self, deps: T) -> Self {
676 self.deps =
677 Some(serde_json::to_vec(&deps).expect("resource deps serialization must succeed"));
678 self
679 }
680
681 pub fn preserve_on_change(mut self) -> Self {
682 self.policy = ResourcePolicy::PreserveOnChange;
683 self
684 }
685
686 pub fn restart_on_change(mut self) -> Self {
687 self.policy = ResourcePolicy::RestartOnChange;
688 self
689 }
690
691 pub fn immediate(mut self) -> Self {
692 self.immediate = true;
693 self
694 }
695
696 pub fn on_tick(mut self, action: ActionEnvelope) -> Self {
697 self.on_tick = Some(action);
698 self
699 }
700}
701
702#[derive(Default)]
703pub struct ResourceRegistry {
704 declarations: Vec<RuntimeResourceDeclaration>,
705 seen_keys: HashMap<String, usize>,
706}
707
708impl ResourceRegistry {
709 pub fn new() -> Self {
710 Self::default()
711 }
712
713 pub fn job(&mut self, resource: JobResource) {
714 self.push(RuntimeResourceDeclaration {
715 key: resource.key.as_str().to_string(),
716 deps: resource.deps.clone(),
717 policy: resource.policy,
718 kind: RuntimeResourceKind::Job(resource),
719 });
720 }
721
722 pub fn service(&mut self, resource: ServiceResource) {
723 self.push(RuntimeResourceDeclaration {
724 key: resource.key.as_str().to_string(),
725 deps: resource.deps.clone(),
726 policy: resource.policy,
727 kind: RuntimeResourceKind::Service(resource),
728 });
729 }
730
731 pub fn timer(&mut self, resource: TimerResource) {
732 self.push(RuntimeResourceDeclaration {
733 key: resource.key.as_str().to_string(),
734 deps: resource.deps.clone(),
735 policy: resource.policy,
736 kind: RuntimeResourceKind::Timer(resource),
737 });
738 }
739
740 pub fn take(&mut self) -> Vec<RuntimeResourceDeclaration> {
741 self.seen_keys.clear();
742 std::mem::take(&mut self.declarations)
743 }
744
745 fn push(&mut self, declaration: RuntimeResourceDeclaration) {
746 if let Some(index) = self.seen_keys.get(&declaration.key) {
747 panic!(
748 "duplicate runtime resource declaration for key '{}' at index {}",
749 declaration.key, index
750 );
751 }
752 let index = self.declarations.len();
753 self.seen_keys.insert(declaration.key.clone(), index);
754 self.declarations.push(declaration);
755 }
756}
757
758#[cfg(test)]
759mod dispatch_failure_tests {
760 use super::action_dispatch_failure_message;
761 use crate::{ActionId, WidgetId};
762
763 #[test]
764 fn fallback_message_contains_only_sanitized_dispatch_metadata() {
765 let message = action_dispatch_failure_message(
766 ActionId::from_u128(7),
767 WidgetId::from_u128(11),
768 "action_deserialization",
769 );
770
771 assert_eq!(
772 message,
773 "Fission action dispatch failed: action_id=7 target=11 failure_kind=action_deserialization"
774 );
775 }
776}