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