1pub use aper_stateroom::{ClientId, IntentEvent, StateMachineContainerProgram, StateProgram};
2use aper_websocket_client::AperWebSocketStateProgramClient;
3use chrono::Duration;
4use gloo_storage::{SessionStorage, Storage};
5use init_tracing::init_tracing;
6use rand::distributions::Alphanumeric;
7use rand::Rng;
8use std::fmt::Debug;
9use std::marker::PhantomData;
10pub use update_interval::UpdateInterval;
11pub use view::{StateProgramViewComponent, StateProgramViewContext};
12use yew::{html, Component, Html, Properties};
13
14mod init_tracing;
15mod update_interval;
16mod view;
17
18const CONNECTION_TOKEN_KEY: &str = "CONNECTION_TOKEN";
19
20fn get_full_ws_url(path: &str) -> String {
23 let location = web_sys::window().unwrap().location();
24 let host = location.host().unwrap();
25 let path_prefix = location.pathname().unwrap();
26 let ws_protocol = match location.protocol().unwrap().as_str() {
27 "http:" => "ws",
28 "https:" => "wss",
29 scheme => panic!("Unknown scheme: {}", scheme),
30 };
31
32 format!("{}://{}{}{}", ws_protocol, &host, &path_prefix, &path)
33}
34
35#[derive(Properties, Clone)]
37pub struct StateProgramComponentProps<V: StateProgramViewComponent> {
38 pub websocket_url: String,
40 pub _ph: PhantomData<V>,
41}
42
43impl<V: StateProgramViewComponent> StateProgramComponentProps<V> {
44 pub fn new(websocket_url: &str) -> Self {
45 init_tracing();
46
47 StateProgramComponentProps {
48 websocket_url: get_full_ws_url(websocket_url),
49 _ph: PhantomData,
50 }
51 }
52}
53
54impl<V: StateProgramViewComponent> PartialEq for StateProgramComponentProps<V> {
55 fn eq(&self, other: &Self) -> bool {
56 self.websocket_url == other.websocket_url
57 }
58}
59
60#[derive(Debug)]
63pub enum Msg<State: StateProgram> {
64 StateTransition(State::T),
65 SetState(State, Duration, u32),
66 Redraw,
67}
68
69struct InnerState<P: StateProgram> {
70 state: P,
71 offset: Duration,
72 client_id: u32,
73}
74
75pub struct StateProgramComponent<V: StateProgramViewComponent> {
78 client: Option<AperWebSocketStateProgramClient<V::Program>>,
80 state: Option<InnerState<V::Program>>,
81 _ph: PhantomData<V>,
82}
83
84impl<V: StateProgramViewComponent> StateProgramComponent<V> {
85 fn do_connect(&mut self, context: &yew::Context<Self>) {
87 let link = context.link().clone();
88
89 let token = if let Ok(token) = SessionStorage::get::<String>(CONNECTION_TOKEN_KEY) {
90 token
91 } else {
92 let token: String = rand::thread_rng()
93 .sample_iter(&Alphanumeric)
94 .take(24)
95 .map(char::from)
96 .collect();
97
98 SessionStorage::set(CONNECTION_TOKEN_KEY, &token).expect("Couldn't set session state.");
99 token
100 };
101
102 let url = format!("{}?token={}", context.props().websocket_url, token);
103
104 let client = AperWebSocketStateProgramClient::new(&url, move |state, client_id| {
105 let offset = Duration::zero();
107
108 link.send_message(Msg::SetState(state, offset, client_id));
109 })
110 .unwrap();
111 self.client = Some(client);
112 }
113}
114
115impl<V: StateProgramViewComponent> Component for StateProgramComponent<V> {
116 type Message = Msg<V::Program>;
117 type Properties = StateProgramComponentProps<V>;
118
119 fn create(context: &yew::Context<Self>) -> Self {
122 let mut result = Self {
123 client: None,
124 state: None,
125 _ph: PhantomData,
126 };
127
128 result.do_connect(context);
129
130 result
131 }
132
133 fn update(&mut self, _: &yew::Context<Self>, msg: Self::Message) -> bool {
134 match msg {
135 Msg::StateTransition(intent) => {
136 self.client.as_mut().unwrap().push_intent(intent).unwrap();
137 false
138 }
139 Msg::SetState(state, offset, client_id) => {
140 self.state = Some(InnerState {
141 state,
142 offset,
143 client_id,
144 });
145 true
146 }
147 Msg::Redraw => true,
148 }
149 }
150
151 fn view(&self, context: &yew::Context<Self>) -> Html {
152 if let Some(inner_state) = &self.state {
153 let InnerState {
154 state,
155 offset,
156 client_id,
157 } = inner_state;
158
159 let context = StateProgramViewContext {
160 callback: context.link().callback(Msg::StateTransition),
161 redraw: context.link().callback(|_| Msg::Redraw),
162 client_id: *client_id,
163 offset: *offset,
164 };
165
166 V::view(state, context)
167 } else {
168 html! {{"Waiting for initial state."}}
169 }
170 }
171}