use std::time::Duration;
use bevy::platform::time::Instant;
use bevy::prelude::Component;
use bevy::prelude::Reflect;
use bevy::prelude::ReflectComponent;
use crate::ApplyPermit;
use crate::BindingGeneration;
use crate::DeviceAccessError;
use crate::DeviceEndpoint;
use crate::DeviceId;
use crate::DeviceRevision;
use crate::DeviceRevisionLookup;
use crate::RetryOn;
use crate::RoleKey;
use crate::reconcile::FrameClockReading;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, Reflect)]
#[reflect(opaque)]
pub struct AttemptId(u64);
impl AttemptId {
pub(crate) const fn new(value: u64) -> Self { Self(value) }
pub(crate) const fn value(self) -> u64 { self.0 }
}
#[derive(Clone, Debug, Reflect)]
pub struct Attempt {
pub id: AttemptId,
pub role: RoleKey,
pub endpoint: DeviceEndpoint,
pub binding_generation: BindingGeneration,
pub permit: ApplyPermit,
pub expected_device_id: DeviceId,
pub device_revision: DeviceRevision,
pub deadline: Instant,
}
#[derive(Clone, PartialEq, Eq, Debug, Reflect)]
pub enum AttemptProgress {
Pending,
Finished(AttemptOutcome),
}
#[derive(Clone, PartialEq, Eq, Debug, Reflect)]
pub enum AttemptOutcome {
Succeeded,
Failed(DeviceAccessError),
Aborted,
Substituted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum AttemptInvalidation {
DeviceChanged,
RevisionAdvanced,
ClaimLost,
IdentityNoLongerConfirmed,
DeviceNotPresent,
InventoryWithdrewTheDevice,
OverrunExhausted,
RoleRetired,
BindingReplaced,
}
#[derive(Component, Clone, PartialEq, Eq, Debug, Reflect)]
#[reflect(Component, PartialEq)]
pub struct LastAttemptEnding {
pub outcome: AttemptOutcome,
pub invalidation: Option<AttemptInvalidation>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Reflect)]
pub(crate) enum AttemptDeadlineStatus {
NoSuchAttempt,
WithinDeadline,
OverdueWithinOverrun {
past_deadline: Duration,
},
OverrunExhausted {
past_deadline: Duration,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RetryGate {
AwaitingRevision(DeviceRevisionLookup),
AwaitingInstant(Instant),
}
impl RetryGate {
pub(crate) fn from_policy(
retry: RetryOn,
device_revision: DeviceRevisionLookup,
now: FrameClockReading,
) -> Self {
match (retry, now) {
(RetryOn::Interval(interval), FrameClockReading::Measurable(now)) => {
Self::AwaitingInstant(now + interval)
},
(RetryOn::Interval(_) | RetryOn::NewRevision, _) => {
Self::AwaitingRevision(device_revision)
},
}
}
pub(crate) fn opened(
self,
device_revision: DeviceRevisionLookup,
now: FrameClockReading,
) -> bool {
match self {
Self::AwaitingRevision(failed_at) => device_revision != failed_at,
Self::AwaitingInstant(retry_at) => match now {
FrameClockReading::Measurable(now) => now >= retry_at,
FrameClockReading::NotYetAdvanced => false,
},
}
}
}
#[derive(Default, Reflect)]
pub enum LastKnownGoodConfiguration {
#[default]
NotEstablished,
Known(#[reflect(ignore, default = "default_erased_configuration")] Box<dyn Reflect>),
}
impl LastKnownGoodConfiguration {
#[must_use]
pub fn known(configuration: impl Reflect) -> Self { Self::Known(Box::new(configuration)) }
pub(crate) fn holds_same_value(&self, other: &Self) -> bool {
match (self, other) {
(Self::NotEstablished, Self::NotEstablished) => true,
(Self::Known(held), Self::Known(captured)) => {
held.reflect_partial_eq(captured.as_partial_reflect()) == Some(true)
},
_ => false,
}
}
pub(crate) fn as_reflect(&self) -> Result<&dyn Reflect, LastKnownGoodConfigurationAccessError> {
match self {
Self::NotEstablished => Err(LastKnownGoodConfigurationAccessError::NotEstablished),
Self::Known(configuration) => Ok(configuration.as_ref()),
}
}
}
pub(crate) enum LastKnownGoodConfigurationAccessError {
NotEstablished,
}
fn default_erased_configuration() -> Box<dyn Reflect> { Box::new(()) }
#[cfg(test)]
mod tests {
use std::any::TypeId;
use bevy::app::App;
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::ecs::reflect::ReflectComponent;
use bevy::prelude::Reflect;
use bevy::reflect::FromReflect;
use bevy::reflect::ReflectFromReflect;
use bevy::reflect::tuple_struct::DynamicTupleStruct;
use super::AttemptId;
use super::LastAttemptEnding;
use super::LastKnownGoodConfiguration;
#[derive(Debug, PartialEq, Eq, Reflect)]
struct ProviderConfiguration {
frame_rate: u32,
}
#[derive(Reflect)]
struct LastKnownGoodConfigurationRecord {
last_known_good_configuration: LastKnownGoodConfiguration,
}
#[test]
fn the_last_attempt_ending_component_registers_reflection_metadata() {
let app = App::new();
let type_registry = app.world().resource::<AppTypeRegistry>().read();
let type_id = TypeId::of::<LastAttemptEnding>();
assert!(type_registry.contains(type_id));
assert!(
type_registry
.get_type_data::<ReflectComponent>(type_id)
.is_some()
);
drop(type_registry);
}
#[test]
fn last_known_good_configuration_allows_its_enclosing_record_to_reflect() {
fn assert_from_reflect<T: FromReflect>() {}
assert_from_reflect::<LastKnownGoodConfigurationRecord>();
let app = App::new();
let type_registry = app.world().resource::<AppTypeRegistry>().read();
let type_id = TypeId::of::<LastKnownGoodConfigurationRecord>();
assert!(type_registry.contains(type_id));
assert!(
type_registry
.get_type_data::<ReflectFromReflect>(type_id)
.is_some()
);
drop(type_registry);
}
#[test]
fn known_configuration_recovers_the_provider_value_after_erasure() {
let last_known_good =
LastKnownGoodConfiguration::known(ProviderConfiguration { frame_rate: 60 });
let recovered = last_known_good.as_reflect().ok().and_then(|configuration| {
configuration
.as_any()
.downcast_ref::<ProviderConfiguration>()
});
assert_eq!(recovered, Some(&ProviderConfiguration { frame_rate: 60 }));
}
#[test]
fn last_known_good_configuration_defaults_to_not_established() {
assert!(matches!(
LastKnownGoodConfiguration::default(),
LastKnownGoodConfiguration::NotEstablished
));
}
#[test]
fn runtime_reflection_cannot_construct_an_attempt_identifier_the_registry_never_issued() {
let mut dynamic_attempt = DynamicTupleStruct::default();
dynamic_attempt.insert(7_u64);
assert!(AttemptId::from_reflect(&dynamic_attempt).is_none());
}
}