use std::cell::RefCell;
use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use brep_render::engine_state::EngineState;
use super::backend::SavedHook;
use super::client::{PlmClient, PlmError};
use super::thumbnail::content_hash;
pub const STALE: &str = "player data is from a different document";
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
pub struct Stats {
pub staged: u32,
pub uploaded: u32,
pub skipped: u32,
pub failed: u32,
pub last_error: String,
}
thread_local! {
static STAGED: RefCell<BTreeMap<String, Staged>> = RefCell::new(BTreeMap::new());
static WAITING: RefCell<Vec<Waiting>> = const { RefCell::new(Vec::new()) };
static STATS: RefCell<Stats> = RefCell::new(Stats::default());
static NOTICE: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn stats() -> Stats {
STATS.with(|s| s.borrow().clone())
}
fn count(f: impl FnOnce(&mut Stats)) {
STATS.with(|s| f(&mut s.borrow_mut()));
}
pub fn take_notice() -> Option<String> {
NOTICE.with(|n| n.borrow_mut().take())
}
struct Staged {
hash: String,
data: Data,
}
struct Waiting {
doc: u64,
after: u64,
data: Data,
}
#[derive(Clone, Default)]
struct Data(Rc<RefCell<Slot>>);
#[derive(Default)]
struct Slot {
json: Option<Option<String>>,
waker: Option<Waker>,
}
impl Data {
fn fill(&self, json: Option<String>) {
let mut slot = self.0.borrow_mut();
slot.json = Some(json);
if let Some(waker) = slot.waker.take() {
waker.wake();
}
}
}
impl Future for Data {
type Output = Option<String>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut slot = self.0.borrow_mut();
match slot.json.take() {
Some(json) => Poll::Ready(json),
None => {
slot.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
}
fn make(engine: &EngineState) -> Option<String> {
let made = if engine.is_instructions() { engine.instructions_player_data() } else { engine.simulation_player_data_baked() };
match made {
Ok(data) => Some(data.to_json()),
Err(why) => {
count(|s| {
s.skipped += 1;
s.last_error = format!("no player data: {why}");
});
None
}
}
}
pub fn stage(key: &str, document: &str, engine: &EngineState, doc: u64) {
if !engine.is_instructions() && engine.simulation_playback_json().is_null() {
return;
}
let data = Data::default();
if engine.run_pending() {
WAITING.with(|w| w.borrow_mut().push(Waiting { doc, after: engine.applied_generation(), data: data.clone() }));
} else {
data.fill(make(engine));
}
count(|s| s.staged += 1);
STAGED.with(|s| s.borrow_mut().insert(key.to_string(), Staged { hash: content_hash(document), data }));
}
pub fn tick<'a>(engine_of: impl Fn(u64) -> Option<&'a EngineState>) {
if WAITING.with(|w| w.borrow().is_empty()) {
return;
}
let mut ready: Vec<(Data, Option<String>)> = Vec::new();
WAITING.with(|w| {
w.borrow_mut().retain(|waiting| match engine_of(waiting.doc) {
None => {
ready.push((waiting.data.clone(), None));
false
}
Some(engine) if engine.run_pending() || engine.applied_generation() == waiting.after => true,
Some(engine) => {
ready.push((waiting.data.clone(), make(engine)));
false
}
})
});
for (data, json) in ready {
data.fill(json);
}
}
fn take(key: &str, document: &str) -> Option<Data> {
let hash = content_hash(document);
STAGED.with(|s| {
let mut staged = s.borrow_mut();
match staged.get(key) {
Some(entry) if entry.hash == hash => staged.remove(key).map(|e| e.data),
_ => None,
}
})
}
pub(crate) fn upload_on_save() -> SavedHook {
Rc::new(|client, key, value| {
let key = crate::plm::uses::revision_key_in(&key).unwrap_or(key);
if let Some(data) = take(&key, &value) {
let hash = content_hash(&value);
crate::store::spawn_waking(async move { upload(&client, &key, &hash, data).await });
}
Box::pin(async { Ok(()) })
})
}
async fn upload(client: &PlmClient, key: &str, hash: &str, data: Data) {
let Some(json) = data.await else {
count(|s| s.skipped += 1);
return;
};
match put(client, key, hash, json).await {
Ok(()) => count(|s| s.uploaded += 1),
Err(error) => {
let why = if error.status() == Some(409) { STALE.to_string() } else { format!("player data not uploaded: {error}") };
log::warn!("{key}: {why}");
count(|s| {
s.failed += 1;
s.last_error = why.clone();
});
NOTICE.with(|n| *n.borrow_mut() = Some(why));
}
}
}
pub async fn put(client: &PlmClient, key: &str, hash: &str, json: String) -> Result<(), PlmError> {
let (part, revision) = super::family::key_ids(key).ok_or_else(|| PlmError::Malformed(format!("'{key}' is not a revision key")))?;
let path = format!("{}/player-data?content_hash={hash}", super::identity::revision_path(&part, &revision));
client.call_typed("PUT", &path, json.into_bytes(), "application/json").await.map(drop)
}