brep_app/plm/
player_data.rs1use std::cell::RefCell;
28use std::collections::BTreeMap;
29use std::future::Future;
30use std::pin::Pin;
31use std::rc::Rc;
32use std::task::{Context, Poll, Waker};
33
34use brep_render::engine_state::EngineState;
35
36use super::backend::SavedHook;
37use super::client::{PlmClient, PlmError};
38use super::content_hash;
39
40pub const STALE: &str = "player data is from a different document";
43
44#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
46pub struct Stats {
47 pub staged: u32,
49 pub uploaded: u32,
51 pub skipped: u32,
53 pub failed: u32,
55 pub last_error: String,
57}
58
59thread_local! {
60 static STAGED: RefCell<BTreeMap<String, Staged>> = RefCell::new(BTreeMap::new());
61 static WAITING: RefCell<Vec<Waiting>> = const { RefCell::new(Vec::new()) };
62 static STATS: RefCell<Stats> = RefCell::new(Stats::default());
63 static NOTICE: RefCell<Option<String>> = const { RefCell::new(None) };
64}
65
66pub fn stats() -> Stats {
68 STATS.with(|s| s.borrow().clone())
69}
70
71fn count(f: impl FnOnce(&mut Stats)) {
72 STATS.with(|s| f(&mut s.borrow_mut()));
73}
74
75pub fn take_notice() -> Option<String> {
77 NOTICE.with(|n| n.borrow_mut().take())
78}
79
80struct Staged {
81 hash: String,
82 data: Data,
83}
84
85struct Waiting {
88 doc: u64,
89 after: u64,
90 data: Data,
91}
92
93#[derive(Clone, Default)]
95struct Data(Rc<RefCell<Slot>>);
96
97#[derive(Default)]
98struct Slot {
99 json: Option<Option<String>>,
101 waker: Option<Waker>,
102}
103
104impl Data {
105 fn fill(&self, json: Option<String>) {
106 let mut slot = self.0.borrow_mut();
107 slot.json = Some(json);
108 if let Some(waker) = slot.waker.take() {
109 waker.wake();
110 }
111 }
112}
113
114impl Future for Data {
115 type Output = Option<String>;
116
117 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
118 let mut slot = self.0.borrow_mut();
119 match slot.json.take() {
120 Some(json) => Poll::Ready(json),
121 None => {
122 slot.waker = Some(cx.waker().clone());
123 Poll::Pending
124 }
125 }
126 }
127}
128
129fn make(engine: &EngineState) -> Option<String> {
133 let made = if engine.is_instructions() { engine.instructions_player_data() } else { engine.simulation_player_data_baked() };
134 match made {
135 Ok(data) => Some(data.to_json()),
136 Err(why) => {
137 count(|s| {
138 s.skipped += 1;
139 s.last_error = format!("no player data: {why}");
140 });
141 None
142 }
143 }
144}
145
146pub fn stage(key: &str, document: &str, engine: &EngineState, doc: u64) {
151 if !engine.is_instructions() && engine.simulation_playback_json().is_null() {
154 return;
155 }
156 let data = Data::default();
157 if engine.run_pending() {
158 WAITING.with(|w| w.borrow_mut().push(Waiting { doc, after: engine.applied_generation(), data: data.clone() }));
159 } else {
160 data.fill(make(engine));
161 }
162 count(|s| s.staged += 1);
163 STAGED.with(|s| s.borrow_mut().insert(key.to_string(), Staged { hash: content_hash(document.as_bytes()), data }));
164}
165
166pub fn tick<'a>(engine_of: impl Fn(u64) -> Option<&'a EngineState>) {
170 if WAITING.with(|w| w.borrow().is_empty()) {
171 return;
172 }
173 let mut ready: Vec<(Data, Option<String>)> = Vec::new();
174 WAITING.with(|w| {
175 w.borrow_mut().retain(|waiting| match engine_of(waiting.doc) {
176 None => {
177 ready.push((waiting.data.clone(), None));
178 false
179 }
180 Some(engine) if engine.run_pending() || engine.applied_generation() == waiting.after => true,
181 Some(engine) => {
182 ready.push((waiting.data.clone(), make(engine)));
183 false
184 }
185 })
186 });
187 for (data, json) in ready {
188 data.fill(json);
189 }
190}
191
192fn take(key: &str, document: &str) -> Option<Data> {
195 let hash = content_hash(document.as_bytes());
196 STAGED.with(|s| {
197 let mut staged = s.borrow_mut();
198 match staged.get(key) {
199 Some(entry) if entry.hash == hash => staged.remove(key).map(|e| e.data),
200 _ => None,
201 }
202 })
203}
204
205pub(crate) fn upload_on_save() -> SavedHook {
208 Rc::new(|client, key, value| {
209 let key = crate::plm::uses::revision_key_in(&key).unwrap_or(key);
210 if let Some(data) = take(&key, &value) {
211 let hash = content_hash(value.as_bytes());
212 crate::store::spawn_waking(async move { upload(&client, &key, &hash, data).await });
213 }
214 Box::pin(async { Ok(()) })
215 })
216}
217
218async fn upload(client: &PlmClient, key: &str, hash: &str, data: Data) {
219 let Some(json) = data.await else {
220 count(|s| s.skipped += 1);
221 return;
222 };
223 match put(client, key, hash, json).await {
224 Ok(()) => count(|s| s.uploaded += 1),
225 Err(error) => {
226 let why = if error.status() == Some(409) { STALE.to_string() } else { format!("player data not uploaded: {error}") };
227 log::warn!("{key}: {why}");
228 count(|s| {
229 s.failed += 1;
230 s.last_error = why.clone();
231 });
232 NOTICE.with(|n| *n.borrow_mut() = Some(why));
233 }
234 }
235}
236
237pub async fn put(client: &PlmClient, key: &str, hash: &str, json: String) -> Result<(), PlmError> {
240 let (part, revision) = super::family::key_ids(key).ok_or_else(|| PlmError::Malformed(format!("'{key}' is not a revision key")))?;
241 let path = format!("{}/player-data?content_hash={hash}", super::identity::revision_path(&part, &revision));
242 client.call_typed("PUT", &path, json.into_bytes(), "application/json").await.map(drop)
243}
244