Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
21 kB · 629 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630//! Shared compare-and-swap load/readiness state machine.//!//! The worker and the main-thread viewer each own an instance of [`LoadReducer`].//! The main thread allocates identities; reducers only accept an identity that was//! supplied by the caller and reject late or cross-namespace events.
use serde::{Deserialize, Serialize};
/// Renderer state namespace. Preview work is deliberately not interactive work.#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]pub enum RendererNamespace { Interactive, Preview,}
/// Monotonic identity allocated by the main thread for one renderer namespace.#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)]pub struct LoadIdentity { pub load_epoch: u32, pub scene_generation: u32,}
impl LoadIdentity { pub const fn new(load_epoch: u32, scene_generation: u32) -> Self { Self { load_epoch, scene_generation, } }}
/// An opaque resource handle used only to communicate disposal obligations./// The resource-owning side chooses the token and maps it to its real resource.#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]pub struct ResourceToken(pub u64);
/// Stable machine-readable failure classes.#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]pub enum LoadErrorCode { Fetch, Parse, Install, Cancelled, Renderer, Unknown,}
/// A stable error code with a human-facing detail string.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]pub struct LoadError { pub code: LoadErrorCode, pub detail: String,}
/// Externally observable reducer states.#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]pub enum LoadState { Idle, Loading(LoadIdentity), Installing(LoadIdentity), Ready(LoadIdentity), Failed(u32, LoadErrorCode), Cancelled(u32), Superseded(u32), Disposed,}
impl LoadState { pub fn identity(&self) -> Option<LoadIdentity> { match self { Self::Loading(id) | Self::Installing(id) | Self::Ready(id) => Some(*id), Self::Failed(epoch, _) | Self::Cancelled(epoch) | Self::Superseded(epoch) => { Some(LoadIdentity::new(*epoch, 0)) } Self::Idle | Self::Disposed => None, } }
pub fn is_session_state(&self) -> bool { !matches!(self, Self::Disposed) }}
/// Reducer input. Resource tokens are candidates owned by the event producer.#[derive(Debug, Clone, PartialEq, Eq)]pub enum LoadEvent { StartLoad { namespace: RendererNamespace, identity: LoadIdentity, }, LoadSucceeded { namespace: RendererNamespace, identity: LoadIdentity, candidate: Option<ResourceToken>, }, LoadFailed { namespace: RendererNamespace, identity: LoadIdentity, error: LoadError, candidate: Option<ResourceToken>, }, InstallSucceeded { namespace: RendererNamespace, identity: LoadIdentity, scene: Option<ResourceToken>, }, InstallFailed { namespace: RendererNamespace, identity: LoadIdentity, error: LoadError, candidate: Option<ResourceToken>, }, Cancel { namespace: RendererNamespace, identity: LoadIdentity, candidate: Option<ResourceToken>, }, Resize { namespace: RendererNamespace, }, Dispose { namespace: RendererNamespace, },}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TransitionOutcome { Committed, IgnoredLateOrStale,}
#[derive(Debug, Clone, PartialEq, Eq)]pub struct TransitionResult { pub outcome: TransitionOutcome, pub state: LoadState, pub disposed: Vec<ResourceToken>,}
impl TransitionResult { fn committed(state: LoadState, disposed: Vec<ResourceToken>) -> Self { Self { outcome: TransitionOutcome::Committed, state, disposed, } }
fn ignored(state: LoadState, candidate: Option<ResourceToken>) -> Self { Self { outcome: TransitionOutcome::IgnoredLateOrStale, state, disposed: candidate.into_iter().collect(), } }}
/// One namespace-local CAS reducer. Identity allocation is intentionally absent.#[derive(Debug, Clone)]pub struct LoadReducer { namespace: RendererNamespace, state: LoadState, current_identity: Option<LoadIdentity>, candidate: Option<ResourceToken>, current_scene: Option<ResourceToken>,}
impl LoadReducer { pub fn new(namespace: RendererNamespace) -> Self { Self { namespace, state: LoadState::Idle, current_identity: None, candidate: None, current_scene: None, } }
pub fn namespace(&self) -> RendererNamespace { self.namespace } pub fn state(&self) -> &LoadState { &self.state } pub fn current_identity(&self) -> Option<LoadIdentity> { self.current_identity } pub fn current_scene(&self) -> Option<ResourceToken> { self.current_scene }
pub fn reduce(&mut self, event: LoadEvent) -> TransitionResult { let (namespace, identity, candidate) = event_metadata(&event); if namespace != self.namespace { return TransitionResult::ignored(self.state.clone(), candidate); } if matches!(event, LoadEvent::Dispose { .. }) { let disposed = self.take_resources(candidate); self.state = LoadState::Disposed; self.current_identity = None; return TransitionResult::committed(LoadState::Disposed, disposed); } if matches!(self.state, LoadState::Disposed) { return TransitionResult::ignored(self.state.clone(), candidate); }
if let LoadEvent::StartLoad { identity, .. } = event { if self .current_identity .is_none_or(|current| identity > current) { let mut disposed = self.take_resources(None); self.state = LoadState::Loading(identity); self.current_identity = Some(identity); disposed.extend(candidate); return TransitionResult::committed(self.state.clone(), disposed); } return TransitionResult::ignored(self.state.clone(), None); }
if matches!(event, LoadEvent::Resize { .. }) { return TransitionResult::committed(self.state.clone(), Vec::new()); } let Some(identity) = identity else { return TransitionResult::ignored(self.state.clone(), candidate); }; if self.current_identity != Some(identity) { return TransitionResult::ignored(self.state.clone(), candidate); }
match event { LoadEvent::LoadSucceeded { candidate, .. } if matches!(self.state, LoadState::Loading(_)) => { self.candidate = candidate; self.state = LoadState::Installing(identity); TransitionResult::committed(self.state.clone(), Vec::new()) } LoadEvent::LoadFailed { error, candidate, .. } if matches!(self.state, LoadState::Loading(_) | LoadState::Installing(_)) => { let disposed = self.take_resources(candidate); self.state = LoadState::Failed(identity.load_epoch, error.code); TransitionResult::committed(self.state.clone(), disposed) } LoadEvent::Cancel { candidate, .. } if matches!( self.state, LoadState::Loading(_) | LoadState::Installing(_) | LoadState::Ready(_) ) => { let disposed = self.take_resources(candidate); self.current_scene = None; self.state = LoadState::Cancelled(identity.load_epoch); TransitionResult::committed(self.state.clone(), disposed) } LoadEvent::InstallSucceeded { scene, .. } if matches!(self.state, LoadState::Installing(_)) => { let mut disposed = self.take_resources(None); if let Some(candidate) = self.candidate.take() { disposed.push(candidate); } self.current_scene = scene; self.state = LoadState::Ready(identity); TransitionResult::committed(self.state.clone(), disposed) } LoadEvent::InstallFailed { error, candidate, .. } if matches!(self.state, LoadState::Installing(_)) => { let disposed = self.take_resources(candidate); self.state = LoadState::Failed(identity.load_epoch, error.code); TransitionResult::committed(self.state.clone(), disposed) } LoadEvent::Resize { .. } => TransitionResult::committed(self.state.clone(), Vec::new()), _ => TransitionResult::ignored(self.state.clone(), candidate), } }
fn take_resources(&mut self, extra: Option<ResourceToken>) -> Vec<ResourceToken> { let mut disposed = Vec::new(); if let Some(token) = self.candidate.take() { disposed.push(token); } if let Some(token) = self.current_scene.take() { disposed.push(token); } if let Some(token) = extra { disposed.push(token); } disposed.sort_unstable_by_key(|token| token.0); disposed.dedup(); disposed }}
fn event_metadata( event: &LoadEvent,) -> ( RendererNamespace, Option<LoadIdentity>, Option<ResourceToken>,) { match event { LoadEvent::StartLoad { namespace, identity, } => (*namespace, Some(*identity), None), LoadEvent::LoadSucceeded { namespace, identity, candidate, } | LoadEvent::LoadFailed { namespace, identity, candidate, .. } | LoadEvent::InstallFailed { namespace, identity, candidate, .. } | LoadEvent::Cancel { namespace, identity, candidate, } => (*namespace, Some(*identity), *candidate), LoadEvent::InstallSucceeded { namespace, identity, scene, } => (*namespace, Some(*identity), *scene), LoadEvent::Resize { namespace } | LoadEvent::Dispose { namespace } => { (*namespace, None, None) } }}
#[cfg(test)]mod tests { use super::*;
const NS: RendererNamespace = RendererNamespace::Interactive; const A: LoadIdentity = LoadIdentity::new(1, 1); const B: LoadIdentity = LoadIdentity::new(1, 2); fn err(code: LoadErrorCode) -> LoadError { LoadError { code, detail: "detail".into(), } } fn start(r: &mut LoadReducer, id: LoadIdentity) { assert_eq!( r.reduce(LoadEvent::StartLoad { namespace: NS, identity: id }) .outcome, TransitionOutcome::Committed ); }
#[test] fn full_install_is_the_only_ready_publication() { let mut r = LoadReducer::new(NS); start(&mut r, A); assert!(matches!(r.state(), LoadState::Loading(A))); r.reduce(LoadEvent::LoadSucceeded { namespace: NS, identity: A, candidate: Some(ResourceToken(1)), }); assert!(matches!(r.state(), LoadState::Installing(A))); assert!(!matches!(r.state(), LoadState::Ready(_))); r.reduce(LoadEvent::InstallSucceeded { namespace: NS, identity: A, scene: Some(ResourceToken(2)), }); assert!(matches!(r.state(), LoadState::Ready(A))); assert_eq!(r.current_scene(), Some(ResourceToken(2))); }
#[test] fn stale_success_is_rejected_and_disposed_after_supersession() { let mut r = LoadReducer::new(NS); start(&mut r, A); start(&mut r, B); let result = r.reduce(LoadEvent::LoadSucceeded { namespace: NS, identity: A, candidate: Some(ResourceToken(9)), }); assert_eq!(result.outcome, TransitionOutcome::IgnoredLateOrStale); assert_eq!(result.disposed, vec![ResourceToken(9)]); assert!(matches!(r.state(), LoadState::Loading(B))); }
#[test] fn cancel_and_dispose_win_against_late_install() { let mut r = LoadReducer::new(NS); start(&mut r, A); r.reduce(LoadEvent::LoadSucceeded { namespace: NS, identity: A, candidate: Some(ResourceToken(1)), }); r.reduce(LoadEvent::Cancel { namespace: NS, identity: A, candidate: None, }); let late = r.reduce(LoadEvent::InstallSucceeded { namespace: NS, identity: A, scene: Some(ResourceToken(2)), }); assert_eq!(late.outcome, TransitionOutcome::IgnoredLateOrStale); assert_eq!(late.disposed, vec![ResourceToken(2)]); let disposed = r.reduce(LoadEvent::Dispose { namespace: NS }); assert_eq!(disposed.state, LoadState::Disposed); assert_eq!( r.reduce(LoadEvent::StartLoad { namespace: NS, identity: B }) .outcome, TransitionOutcome::IgnoredLateOrStale ); }
#[test] fn preview_and_interactive_reducers_are_isolated() { let interactive = RendererNamespace::Interactive; let preview = RendererNamespace::Preview; let identity = LoadIdentity::new(3, 1); let mut interactive_reducer = LoadReducer::new(interactive); let mut preview_reducer = LoadReducer::new(preview);
assert_eq!( interactive_reducer .reduce(LoadEvent::StartLoad { namespace: interactive, identity, }) .outcome, TransitionOutcome::Committed ); assert_eq!( preview_reducer .reduce(LoadEvent::StartLoad { namespace: preview, identity, }) .outcome, TransitionOutcome::Committed ); assert_eq!( preview_reducer .reduce(LoadEvent::LoadSucceeded { namespace: interactive, identity, candidate: Some(ResourceToken(8)), }) .outcome, TransitionOutcome::IgnoredLateOrStale ); assert_eq!(preview_reducer.state(), &LoadState::Loading(identity));
preview_reducer.reduce(LoadEvent::LoadSucceeded { namespace: preview, identity, candidate: None, }); // Preview acquisition reaches the handoff boundary without installing // a renderable scene in either reducer. assert_eq!(preview_reducer.state(), &LoadState::Installing(identity)); assert_eq!(preview_reducer.current_scene(), None); assert_eq!(interactive_reducer.state(), &LoadState::Loading(identity)); assert_eq!(interactive_reducer.current_scene(), None); }
#[test] fn failure_and_wrong_namespace_are_cas_guarded() { let mut r = LoadReducer::new(NS); start(&mut r, A); r.reduce(LoadEvent::LoadFailed { namespace: NS, identity: A, error: err(LoadErrorCode::Parse), candidate: Some(ResourceToken(3)), }); assert!(matches!( r.state(), LoadState::Failed(1, LoadErrorCode::Parse) )); let result = r.reduce(LoadEvent::StartLoad { namespace: RendererNamespace::Preview, identity: B, }); assert_eq!(result.outcome, TransitionOutcome::IgnoredLateOrStale); }
#[test] fn install_failure_returns_to_failed_state_and_releases_candidate() { let mut r = LoadReducer::new(NS); start(&mut r, A); r.reduce(LoadEvent::LoadSucceeded { namespace: NS, identity: A, candidate: Some(ResourceToken(7)), }); let result = r.reduce(LoadEvent::LoadFailed { namespace: NS, identity: A, error: err(LoadErrorCode::Install), candidate: None, }); assert_eq!(result.outcome, TransitionOutcome::Committed); assert_eq!(result.disposed, vec![ResourceToken(7)]); assert!(matches!( r.state(), LoadState::Failed(1, LoadErrorCode::Install) )); }
fn installing_reducer() -> LoadReducer { let mut r = LoadReducer::new(NS); start(&mut r, A); r.reduce(LoadEvent::LoadSucceeded { namespace: NS, identity: A, candidate: Some(ResourceToken(1)), }); r }
#[test] fn install_first_then_cancel_is_terminal_and_late_install_is_disposed() { let mut r = installing_reducer(); assert!(matches!(r.state(), LoadState::Installing(A))); assert_eq!( r.reduce(LoadEvent::Cancel { namespace: NS, identity: A, candidate: None, }) .state, LoadState::Cancelled(1) ); let late = r.reduce(LoadEvent::InstallSucceeded { namespace: NS, identity: A, scene: Some(ResourceToken(2)), }); assert_eq!(late.outcome, TransitionOutcome::IgnoredLateOrStale); assert_eq!(late.disposed, vec![ResourceToken(2)]); }
#[test] fn install_first_then_supersede_rejects_late_result_for_old_identity() { let mut r = installing_reducer(); start(&mut r, B); assert!(matches!(r.state(), LoadState::Loading(B))); let late = r.reduce(LoadEvent::InstallSucceeded { namespace: NS, identity: A, scene: Some(ResourceToken(3)), }); assert_eq!(late.outcome, TransitionOutcome::IgnoredLateOrStale); assert_eq!(late.disposed, vec![ResourceToken(3)]); }
#[test] fn install_first_then_dispose_rejects_late_result() { let mut r = installing_reducer(); assert_eq!( r.reduce(LoadEvent::Dispose { namespace: NS }).state, LoadState::Disposed ); let late = r.reduce(LoadEvent::InstallSucceeded { namespace: NS, identity: A, scene: Some(ResourceToken(4)), }); assert_eq!(late.outcome, TransitionOutcome::IgnoredLateOrStale); assert_eq!(late.disposed, vec![ResourceToken(4)]); }
#[test] fn ready_then_cancel_clears_current_scene() { let mut r = installing_reducer(); r.reduce(LoadEvent::InstallSucceeded { namespace: NS, identity: A, scene: Some(ResourceToken(2)), }); assert!(matches!(r.state(), LoadState::Ready(A))); let result = r.reduce(LoadEvent::Cancel { namespace: NS, identity: A, candidate: None, }); assert_eq!(result.disposed, vec![ResourceToken(2)]); assert_eq!(r.state(), &LoadState::Cancelled(1)); assert_eq!(r.current_scene(), None); }
#[test] fn resize_during_installing_preserves_state_and_identity() { let mut r = installing_reducer(); let result = r.reduce(LoadEvent::Resize { namespace: NS }); assert_eq!(result.outcome, TransitionOutcome::Committed); assert_eq!(r.current_identity(), Some(A)); assert!(matches!(r.state(), LoadState::Installing(A))); }
#[test] fn resize_never_changes_identity_or_state() { let mut r = LoadReducer::new(NS); start(&mut r, A); let result = r.reduce(LoadEvent::Resize { namespace: NS }); assert_eq!(result.outcome, TransitionOutcome::Committed); assert_eq!(r.current_identity(), Some(A)); assert!(matches!(r.state(), LoadState::Loading(A))); }}