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
20pub type Handler<S, A> = for<'a, 'b, 'c> fn(&mut S, A, &mut ReducerContext<'a, 'b, 'c, S>);
28
29pub trait IntoHandler<S: GlobalState, A> {
33 fn call<'a, 'b, 'c>(&self, state: &mut S, action: A, ctx: &mut ReducerContext<'a, 'b, 'c, S>);
35}
36
37impl<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
44impl<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
53type 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
66pub 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#[derive(Clone, Debug)]
219pub struct VideoRegistration {
220 pub node_id: WidgetId,
222 pub source: String,
224 pub autoplay: bool,
226 pub loop_playback: bool,
228 pub audio: VideoAudioOptions,
230}
231
232#[derive(Clone, Debug)]
234pub struct WebRegistration {
235 pub node_id: WidgetId,
237 pub url: String,
239 pub user_agent: Option<String>,
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
248pub enum PortalLayer {
249 Default = 0,
251 Modal = 100,
253 Flyout = 200,
255 Toast = 300,
257}
258
259#[derive(Clone, Debug)]
263pub struct PortalEntry {
264 pub layer: PortalLayer,
266 pub seq: u64,
268 pub id: Option<WidgetId>,
270 pub node: Widget,
272}
273
274#[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}