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 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#[derive(Clone, Debug)]
227pub struct VideoRegistration {
228 pub node_id: WidgetId,
230 pub source: String,
232 pub autoplay: bool,
234 pub loop_playback: bool,
236 pub audio: VideoAudioOptions,
238}
239
240#[derive(Clone, Debug)]
242pub struct WebRegistration {
243 pub node_id: WidgetId,
245 pub url: String,
247 pub user_agent: Option<String>,
249}
250
251#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
256pub enum PortalLayer {
257 Default = 0,
259 Modal = 100,
261 Flyout = 200,
263 Toast = 300,
265}
266
267#[derive(Clone, Debug)]
271pub struct PortalEntry {
272 pub layer: PortalLayer,
274 pub seq: u64,
276 pub id: Option<WidgetId>,
278 pub node: Widget,
280}
281
282#[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}