removed bincode for rkyv

Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-12-17 19:20:34 +00:00
parent 56f0f0d40b
commit 99e31b1157
47 changed files with 2728 additions and 1697 deletions

View File

@@ -8,24 +8,21 @@ use std::collections::HashMap;
use bevy::prelude::*;
use uuid::Uuid;
use crate::{
networking::{
VectorClock,
blob_support::{
BlobStore,
get_component_data,
},
delta_generation::NodeVectorClock,
entity_map::NetworkEntityMap,
merge::compare_operations_lww,
messages::{
ComponentData,
EntityDelta,
SyncMessage,
},
operations::ComponentOp,
use crate::networking::{
VectorClock,
blob_support::{
BlobStore,
get_component_data,
},
persistence::reflection::deserialize_component_typed,
delta_generation::NodeVectorClock,
entity_map::NetworkEntityMap,
merge::compare_operations_lww,
messages::{
ComponentData,
EntityDelta,
SyncMessage,
},
operations::ComponentOp,
};
/// Resource to track the last vector clock and originating node for each
@@ -177,35 +174,35 @@ pub fn apply_entity_delta(delta: &EntityDelta, world: &mut World) {
fn apply_component_op(entity: Entity, op: &ComponentOp, incoming_node_id: Uuid, world: &mut World) {
match op {
| ComponentOp::Set {
component_type,
discriminant,
data,
vector_clock,
} => {
apply_set_operation_with_lww(
entity,
component_type,
*discriminant,
data,
vector_clock,
incoming_node_id,
world,
);
},
| ComponentOp::SetAdd { component_type, .. } => {
| ComponentOp::SetAdd { discriminant, .. } => {
// OR-Set add - Phase 10 provides OrSet<T> type
// Application code should use OrSet in components and handle SetAdd/SetRemove
// Full integration will be in Phase 12 plugin
debug!(
"SetAdd operation for {} (use OrSet<T> in components)",
component_type
"SetAdd operation for discriminant {} (use OrSet<T> in components)",
discriminant
);
},
| ComponentOp::SetRemove { component_type, .. } => {
| ComponentOp::SetRemove { discriminant, .. } => {
// OR-Set remove - Phase 10 provides OrSet<T> type
// Application code should use OrSet in components and handle SetAdd/SetRemove
// Full integration will be in Phase 12 plugin
debug!(
"SetRemove operation for {} (use OrSet<T> in components)",
component_type
"SetRemove operation for discriminant {} (use OrSet<T> in components)",
discriminant
);
},
| ComponentOp::SequenceInsert { .. } => {
@@ -230,12 +227,26 @@ fn apply_component_op(entity: Entity, op: &ComponentOp, incoming_node_id: Uuid,
/// Uses node_id as a deterministic tiebreaker for concurrent operations.
fn apply_set_operation_with_lww(
entity: Entity,
component_type: &str,
discriminant: u16,
data: &ComponentData,
incoming_clock: &VectorClock,
incoming_node_id: Uuid,
world: &mut World,
) {
// Get component type name for logging and clock tracking
let type_registry = {
let registry_resource = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
registry_resource.0
};
let component_type_name = match type_registry.get_type_name(discriminant) {
| Some(name) => name,
| None => {
error!("Unknown discriminant {} - component not registered", discriminant);
return;
},
};
// Get the network ID for this entity
let entity_network_id = {
if let Ok(entity_ref) = world.get_entity(entity) {
@@ -255,7 +266,7 @@ fn apply_set_operation_with_lww(
let should_apply = {
if let Some(component_clocks) = world.get_resource::<ComponentVectorClocks>() {
if let Some((current_clock, current_node_id)) =
component_clocks.get(entity_network_id, component_type)
component_clocks.get(entity_network_id, component_type_name)
{
// We have a current clock - do LWW comparison with real node IDs
let decision = compare_operations_lww(
@@ -269,14 +280,14 @@ fn apply_set_operation_with_lww(
| crate::networking::merge::MergeDecision::ApplyRemote => {
debug!(
"Applying remote Set for {} (remote is newer)",
component_type
component_type_name
);
true
},
| crate::networking::merge::MergeDecision::KeepLocal => {
debug!(
"Ignoring remote Set for {} (local is newer)",
component_type
component_type_name
);
false
},
@@ -287,19 +298,19 @@ fn apply_set_operation_with_lww(
if incoming_node_id > *current_node_id {
debug!(
"Applying remote Set for {} (concurrent, remote node_id {:?} > local {:?})",
component_type, incoming_node_id, current_node_id
component_type_name, incoming_node_id, current_node_id
);
true
} else {
debug!(
"Ignoring remote Set for {} (concurrent, local node_id {:?} >= remote {:?})",
component_type, current_node_id, incoming_node_id
component_type_name, current_node_id, incoming_node_id
);
false
}
},
| crate::networking::merge::MergeDecision::Equal => {
debug!("Ignoring remote Set for {} (clocks equal)", component_type);
debug!("Ignoring remote Set for {} (clocks equal)", component_type_name);
false
},
}
@@ -307,7 +318,7 @@ fn apply_set_operation_with_lww(
// No current clock - this is the first time we're setting this component
debug!(
"Applying remote Set for {} (no current clock)",
component_type
component_type_name
);
true
}
@@ -323,19 +334,19 @@ fn apply_set_operation_with_lww(
}
// Apply the operation
apply_set_operation(entity, component_type, data, world);
apply_set_operation(entity, discriminant, data, world);
// Update the stored vector clock with node_id
if let Some(mut component_clocks) = world.get_resource_mut::<ComponentVectorClocks>() {
component_clocks.set(
entity_network_id,
component_type.to_string(),
component_type_name.to_string(),
incoming_clock.clone(),
incoming_node_id,
);
debug!(
"Updated vector clock for {} on entity {:?} (node_id: {:?})",
component_type, entity_network_id, incoming_node_id
component_type_name, entity_network_id, incoming_node_id
);
}
}
@@ -346,15 +357,12 @@ fn apply_set_operation_with_lww(
/// Handles both inline data and blob references.
fn apply_set_operation(
entity: Entity,
component_type: &str,
discriminant: u16,
data: &ComponentData,
world: &mut World,
) {
let type_registry = {
let registry_resource = world.resource::<AppTypeRegistry>();
registry_resource.read()
};
let blob_store = world.get_resource::<BlobStore>();
// Get the actual data (resolve blob if needed)
let data_bytes = match data {
| ComponentData::Inline(bytes) => bytes.clone(),
@@ -364,61 +372,58 @@ fn apply_set_operation(
| Ok(bytes) => bytes,
| Err(e) => {
error!(
"Failed to retrieve blob for component {}: {}",
component_type, e
"Failed to retrieve blob for discriminant {}: {}",
discriminant, e
);
return;
},
}
} else {
error!(
"Blob reference for {} but no blob store available",
component_type
"Blob reference for discriminant {} but no blob store available",
discriminant
);
return;
}
},
};
let reflected = match deserialize_component_typed(&data_bytes, component_type, &type_registry) {
| Ok(reflected) => reflected,
// Get component type registry
let type_registry = {
let registry_resource = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
registry_resource.0
};
// Look up deserialize and insert functions by discriminant
let deserialize_fn = type_registry.get_deserialize_fn(discriminant);
let insert_fn = type_registry.get_insert_fn(discriminant);
let (deserialize_fn, insert_fn) = match (deserialize_fn, insert_fn) {
| (Some(d), Some(i)) => (d, i),
| _ => {
error!("Discriminant {} not registered in ComponentTypeRegistry", discriminant);
return;
},
};
// Deserialize the component
let boxed_component = match deserialize_fn(&data_bytes) {
| Ok(component) => component,
| Err(e) => {
error!("Failed to deserialize component {}: {}", component_type, e);
error!("Failed to deserialize discriminant {}: {}", discriminant, e);
return;
},
};
let registration = match type_registry.get_with_type_path(component_type) {
| Some(reg) => reg,
| None => {
error!("Component type {} not registered", component_type);
return;
},
};
let reflect_component = match registration.data::<ReflectComponent>() {
| Some(rc) => rc.clone(),
| None => {
error!(
"Component type {} does not have ReflectComponent data",
component_type
);
return;
},
};
drop(type_registry);
let type_registry_arc = world.resource::<AppTypeRegistry>().clone();
let type_registry_guard = type_registry_arc.read();
// Insert the component into the entity
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
reflect_component.insert(&mut entity_mut, &*reflected, &type_registry_guard);
debug!("Applied Set operation for {}", component_type);
insert_fn(&mut entity_mut, boxed_component);
debug!("Applied Set operation for discriminant {}", discriminant);
// If we just inserted a Transform component, also add NetworkedTransform
// This ensures remote entities can have their Transform changes detected
if component_type == "bevy_transform::components::transform::Transform" {
let type_path = type_registry.get_type_path(discriminant);
if type_path == Some("bevy_transform::components::transform::Transform") {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
if entity_mut
.get::<crate::networking::NetworkedTransform>()
@@ -431,8 +436,8 @@ fn apply_set_operation(
}
} else {
error!(
"Entity {:?} not found when applying component {}",
entity, component_type
"Entity {:?} not found when applying discriminant {}",
entity, discriminant
);
}
}

View File

@@ -94,7 +94,7 @@ pub fn generate_delta_system(world: &mut World) {
// Phase 1: Check and update clocks, collect data
let mut system_state: bevy::ecs::system::SystemState<(
Res<GossipBridge>,
Res<AppTypeRegistry>,
Res<crate::persistence::ComponentTypeRegistryResource>,
ResMut<NodeVectorClock>,
ResMut<LastSyncVersions>,
Option<ResMut<crate::networking::OperationLog>>,
@@ -120,17 +120,16 @@ pub fn generate_delta_system(world: &mut World) {
// Phase 2: Build operations (needs world access without holding other borrows)
let operations = {
let type_registry = world.resource::<AppTypeRegistry>().read();
let ops = build_entity_operations(
let type_registry_res = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
let type_registry = type_registry_res.0;
build_entity_operations(
entity,
world,
node_id,
vector_clock.clone(),
&type_registry,
type_registry,
None, // blob_store - will be added in later phases
);
drop(type_registry);
ops
)
};
if operations.is_empty() {
@@ -175,25 +174,34 @@ pub fn generate_delta_system(world: &mut World) {
// Phase 4: Update component vector clocks for local modifications
{
// Get type registry first before mutable borrow
let type_registry = {
let type_registry_res = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
type_registry_res.0
};
if let Some(mut component_clocks) =
world.get_resource_mut::<crate::networking::ComponentVectorClocks>()
{
for op in &delta.operations {
if let crate::networking::ComponentOp::Set {
component_type,
discriminant,
vector_clock: op_clock,
..
} = op
{
let component_type_name = type_registry.get_type_name(*discriminant)
.unwrap_or("unknown");
component_clocks.set(
network_id,
component_type.clone(),
component_type_name.to_string(),
op_clock.clone(),
node_id,
);
debug!(
"Updated local vector clock for {} on entity {:?} (node_id: {:?})",
component_type, network_id, node_id
component_type_name, network_id, node_id
);
}
}

View File

@@ -64,12 +64,6 @@ impl fmt::Display for NetworkingError {
impl std::error::Error for NetworkingError {}
impl From<bincode::Error> for NetworkingError {
fn from(e: bincode::Error) -> Self {
NetworkingError::Serialization(e.to_string())
}
}
impl From<crate::persistence::PersistenceError> for NetworkingError {
fn from(e: crate::persistence::PersistenceError) -> Self {
NetworkingError::Other(format!("Persistence error: {}", e))

View File

@@ -11,10 +11,7 @@
//! **NOTE:** This is a simplified implementation for Phase 7. Full security
//! and session management will be enhanced in Phase 13.
use bevy::{
prelude::*,
reflect::TypeRegistry,
};
use bevy::prelude::*;
use crate::networking::{
GossipBridge,
@@ -76,7 +73,7 @@ pub fn build_join_request(
///
/// - `world`: Bevy world containing entities
/// - `query`: Query for all NetworkedEntity components
/// - `type_registry`: Type registry for serialization
/// - `type_registry`: Component type registry for serialization
/// - `node_clock`: Current node vector clock
/// - `blob_store`: Optional blob store for large components
///
@@ -86,7 +83,7 @@ pub fn build_join_request(
pub fn build_full_state(
world: &World,
networked_entities: &Query<(Entity, &NetworkedEntity)>,
type_registry: &TypeRegistry,
type_registry: &crate::persistence::ComponentTypeRegistry,
node_clock: &NodeVectorClock,
blob_store: Option<&BlobStore>,
) -> VersionedMessage {
@@ -95,53 +92,31 @@ pub fn build_full_state(
blob_support::create_component_data,
messages::ComponentState,
},
persistence::reflection::serialize_component,
};
let mut entities = Vec::new();
for (entity, networked) in networked_entities.iter() {
let entity_ref = world.entity(entity);
let mut components = Vec::new();
// Iterate over all type registrations to find components
for registration in type_registry.iter() {
// Skip if no ReflectComponent data
let Some(reflect_component) = registration.data::<ReflectComponent>() else {
continue;
// Serialize all registered Synced components on this entity
let serialized_components = type_registry.serialize_entity_components(world, entity);
for (discriminant, _type_path, serialized) in serialized_components {
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
match create_component_data(serialized, store) {
| Ok(d) => d,
| Err(_) => continue,
}
} else {
crate::networking::ComponentData::Inline(serialized)
};
let type_path = registration.type_info().type_path();
// Skip networked wrapper components
if type_path.ends_with("::NetworkedEntity") ||
type_path.ends_with("::NetworkedTransform") ||
type_path.ends_with("::NetworkedSelection") ||
type_path.ends_with("::NetworkedDrawingPath")
{
continue;
}
// Try to reflect this component from the entity
if let Some(reflected) = reflect_component.reflect(entity_ref) {
// Serialize the component
if let Ok(serialized) = serialize_component(reflected, type_registry) {
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
match create_component_data(serialized, store) {
| Ok(d) => d,
| Err(_) => continue,
}
} else {
crate::networking::ComponentData::Inline(serialized)
};
components.push(ComponentState {
component_type: type_path.to_string(),
data,
});
}
}
components.push(ComponentState {
discriminant,
data,
});
}
entities.push(EntityState {
@@ -175,36 +150,32 @@ pub fn build_full_state(
/// - `vector_clock`: Vector clock from FullState
/// - `commands`: Bevy commands for spawning entities
/// - `entity_map`: Entity map to populate
/// - `type_registry`: Type registry for deserialization
/// - `type_registry`: Component type registry for deserialization
/// - `node_clock`: Our node's vector clock to update
/// - `blob_store`: Optional blob store for resolving blob references
/// - `tombstone_registry`: Optional tombstone registry for deletion tracking
pub fn apply_full_state(
entities: Vec<EntityState>,
remote_clock: crate::networking::VectorClock,
commands: &mut Commands,
entity_map: &mut NetworkEntityMap,
type_registry: &TypeRegistry,
node_clock: &mut NodeVectorClock,
blob_store: Option<&BlobStore>,
mut tombstone_registry: Option<&mut crate::networking::TombstoneRegistry>,
world: &mut World,
type_registry: &crate::persistence::ComponentTypeRegistry,
) {
use crate::{
networking::blob_support::get_component_data,
persistence::reflection::deserialize_component,
};
use crate::networking::blob_support::get_component_data;
info!("Applying FullState with {} entities", entities.len());
// Merge the remote vector clock
node_clock.clock.merge(&remote_clock);
{
let mut node_clock = world.resource_mut::<NodeVectorClock>();
node_clock.clock.merge(&remote_clock);
}
// Spawn all entities and apply their state
for entity_state in entities {
// Handle deleted entities (tombstones)
if entity_state.is_deleted {
// Record tombstone
if let Some(ref mut registry) = tombstone_registry {
if let Some(mut registry) = world.get_resource_mut::<crate::networking::TombstoneRegistry>() {
registry.record_deletion(
entity_state.entity_id,
entity_state.owner_node_id,
@@ -216,7 +187,7 @@ pub fn apply_full_state(
// Spawn entity with NetworkedEntity and Persisted components
// This ensures entities received via FullState are persisted locally
let entity = commands
let entity = world
.spawn((
NetworkedEntity::with_id(entity_state.entity_id, entity_state.owner_node_id),
crate::persistence::Persisted::with_id(entity_state.entity_id),
@@ -224,7 +195,10 @@ pub fn apply_full_state(
.id();
// Register in entity map
entity_map.insert(entity_state.entity_id, entity);
{
let mut entity_map = world.resource_mut::<NetworkEntityMap>();
entity_map.insert(entity_state.entity_id, entity);
}
let num_components = entity_state.components.len();
@@ -234,82 +208,56 @@ pub fn apply_full_state(
let data_bytes = match &component_state.data {
| crate::networking::ComponentData::Inline(bytes) => bytes.clone(),
| blob_ref @ crate::networking::ComponentData::BlobRef { .. } => {
if let Some(store) = blob_store {
let blob_store = world.get_resource::<BlobStore>();
if let Some(store) = blob_store.as_deref() {
match get_component_data(blob_ref, store) {
| Ok(bytes) => bytes,
| Err(e) => {
error!(
"Failed to retrieve blob for {}: {}",
component_state.component_type, e
"Failed to retrieve blob for discriminant {}: {}",
component_state.discriminant, e
);
continue;
},
}
} else {
error!(
"Blob reference for {} but no blob store available",
component_state.component_type
"Blob reference for discriminant {} but no blob store available",
component_state.discriminant
);
continue;
}
},
};
// Use the discriminant directly from ComponentState
let discriminant = component_state.discriminant;
// Deserialize the component
let reflected = match deserialize_component(&data_bytes, type_registry) {
| Ok(r) => r,
let boxed_component = match type_registry.deserialize(discriminant, &data_bytes) {
| Ok(component) => component,
| Err(e) => {
error!(
"Failed to deserialize {}: {}",
component_state.component_type, e
"Failed to deserialize discriminant {}: {}",
discriminant, e
);
continue;
},
};
// Get the type registration
let registration =
match type_registry.get_with_type_path(&component_state.component_type) {
| Some(reg) => reg,
| None => {
error!(
"Component type {} not registered",
component_state.component_type
);
continue;
},
};
// Get ReflectComponent data
let reflect_component = match registration.data::<ReflectComponent>() {
| Some(rc) => rc.clone(),
| None => {
error!(
"Component type {} does not have ReflectComponent data",
component_state.component_type
);
continue;
},
// Get the insert function for this discriminant
let Some(insert_fn) = type_registry.get_insert_fn(discriminant) else {
error!("No insert function for discriminant {}", discriminant);
continue;
};
// Insert the component
let component_type_owned = component_state.component_type.clone();
commands.queue(move |world: &mut World| {
let type_registry_arc = {
let Some(type_registry_res) = world.get_resource::<AppTypeRegistry>() else {
error!("AppTypeRegistry not found in world");
return;
};
type_registry_res.clone()
};
let type_registry = type_registry_arc.read();
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
reflect_component.insert(&mut entity_mut, &*reflected, &type_registry);
debug!("Applied component {} from FullState", component_type_owned);
}
});
// Insert the component directly
let type_name_for_log = type_registry.get_type_name(discriminant)
.unwrap_or("unknown");
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
insert_fn(&mut entity_mut, boxed_component);
debug!("Applied component {} from FullState", type_name_for_log);
}
}
debug!(
@@ -337,7 +285,7 @@ pub fn handle_join_requests_system(
world: &World,
bridge: Option<Res<GossipBridge>>,
networked_entities: Query<(Entity, &NetworkedEntity)>,
type_registry: Res<AppTypeRegistry>,
type_registry: Res<crate::persistence::ComponentTypeRegistryResource>,
node_clock: Res<NodeVectorClock>,
blob_store: Option<Res<BlobStore>>,
) {
@@ -345,7 +293,7 @@ pub fn handle_join_requests_system(
return;
};
let registry = type_registry.read();
let registry = type_registry.0;
let blob_store_ref = blob_store.as_deref();
// Poll for incoming JoinRequest messages
@@ -422,21 +370,17 @@ pub fn handle_join_requests_system(
///
/// This system should run BEFORE receive_and_apply_deltas_system to ensure
/// we're fully initialized before processing deltas.
pub fn handle_full_state_system(
mut commands: Commands,
bridge: Option<Res<GossipBridge>>,
mut entity_map: ResMut<NetworkEntityMap>,
type_registry: Res<AppTypeRegistry>,
mut node_clock: ResMut<NodeVectorClock>,
blob_store: Option<Res<BlobStore>>,
mut tombstone_registry: Option<ResMut<crate::networking::TombstoneRegistry>>,
) {
let Some(bridge) = bridge else {
pub fn handle_full_state_system(world: &mut World) {
// Check if bridge exists
if world.get_resource::<GossipBridge>().is_none() {
return;
};
}
let registry = type_registry.read();
let blob_store_ref = blob_store.as_deref();
let bridge = world.resource::<GossipBridge>().clone();
let type_registry = {
let registry_resource = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
registry_resource.0
};
// Poll for FullState messages
while let Some(message) = bridge.try_recv() {
@@ -450,12 +394,8 @@ pub fn handle_full_state_system(
apply_full_state(
entities,
vector_clock,
&mut commands,
&mut entity_map,
&registry,
&mut node_clock,
blob_store_ref,
tombstone_registry.as_deref_mut(),
world,
type_registry,
);
},
| _ => {
@@ -582,29 +522,25 @@ mod tests {
#[test]
fn test_apply_full_state_empty() {
let node_id = uuid::Uuid::new_v4();
let mut node_clock = NodeVectorClock::new(node_id);
let remote_clock = VectorClock::new();
let type_registry = crate::persistence::component_registry();
// Create minimal setup for testing
let mut entity_map = NetworkEntityMap::new();
let type_registry = TypeRegistry::new();
// Need a minimal Bevy app for Commands
// Need a minimal Bevy app for testing
let mut app = App::new();
let mut commands = app.world_mut().commands();
// Insert required resources
app.insert_resource(NetworkEntityMap::new());
app.insert_resource(NodeVectorClock::new(node_id));
apply_full_state(
vec![],
remote_clock.clone(),
&mut commands,
&mut entity_map,
&type_registry,
&mut node_clock,
None,
None, // tombstone_registry
app.world_mut(),
type_registry,
);
// Should have merged clocks
let node_clock = app.world().resource::<NodeVectorClock>();
assert_eq!(node_clock.clock, remote_clock);
}
}

View File

@@ -64,7 +64,7 @@ pub const LOCK_TIMEOUT: Duration = Duration::from_secs(5);
pub const MAX_LOCKS_PER_NODE: usize = 100;
/// Lock acquisition/release messages
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
pub enum LockMessage {
/// Request to acquire a lock on an entity
LockRequest {
@@ -665,8 +665,8 @@ mod tests {
];
for message in messages {
let bytes = bincode::serialize(&message).unwrap();
let deserialized: LockMessage = bincode::deserialize(&bytes).unwrap();
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&message).map(|b| b.to_vec()).unwrap();
let deserialized: LockMessage = rkyv::from_bytes::<LockMessage, rkyv::rancor::Failure>(&bytes).unwrap();
assert_eq!(message, deserialized);
}
}

View File

@@ -217,13 +217,13 @@ mod tests {
let data = vec![1, 2, 3];
let op1 = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(data.clone()),
vector_clock: clock.clone(),
};
let op2 = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(data.clone()),
vector_clock: clock,
};
@@ -244,13 +244,13 @@ mod tests {
clock2.increment(node_id);
let op1 = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(vec![1, 2, 3]),
vector_clock: clock1,
};
let op2 = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(vec![4, 5, 6]),
vector_clock: clock2,
};

View File

@@ -239,41 +239,17 @@ fn dispatch_message(world: &mut World, message: crate::networking::VersionedMess
} => {
info!("Received FullState with {} entities", entities.len());
// Use SystemState to properly borrow multiple resources
let mut system_state: SystemState<(
Commands,
ResMut<NetworkEntityMap>,
Res<AppTypeRegistry>,
ResMut<NodeVectorClock>,
Option<Res<BlobStore>>,
Option<ResMut<TombstoneRegistry>>,
)> = SystemState::new(world);
let type_registry = {
let registry_resource = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
registry_resource.0
};
{
let (
mut commands,
mut entity_map,
type_registry,
mut node_clock,
blob_store,
mut tombstone_registry,
) = system_state.get_mut(world);
let registry = type_registry.read();
apply_full_state(
entities,
vector_clock,
&mut commands,
&mut entity_map,
&registry,
&mut node_clock,
blob_store.as_deref(),
tombstone_registry.as_deref_mut(),
);
// registry is dropped here
}
system_state.apply(world);
apply_full_state(
entities,
vector_clock,
world,
type_registry,
);
},
// SyncRequest - peer requesting missing operations
@@ -433,7 +409,7 @@ fn dispatch_message(world: &mut World, message: crate::networking::VersionedMess
fn build_full_state_from_data(
world: &World,
networked_entities: &[(Entity, &NetworkedEntity)],
type_registry: &bevy::reflect::TypeRegistry,
_type_registry: &bevy::reflect::TypeRegistry,
node_clock: &NodeVectorClock,
blob_store: Option<&BlobStore>,
) -> crate::networking::VersionedMessage {
@@ -445,7 +421,6 @@ fn build_full_state_from_data(
EntityState,
},
},
persistence::reflection::serialize_component,
};
// Get tombstone registry to filter out deleted entities
@@ -464,18 +439,16 @@ fn build_full_state_from_data(
continue;
}
}
let entity_ref = world.entity(*entity);
let mut components = Vec::new();
// Iterate over all type registrations to find components
for registration in type_registry.iter() {
// Skip if no ReflectComponent data
let Some(reflect_component) = registration.data::<ReflectComponent>() else {
continue;
};
// Get component type registry
let type_registry_res = world.resource::<crate::persistence::ComponentTypeRegistryResource>();
let component_registry = type_registry_res.0;
let type_path = registration.type_info().type_path();
// Serialize all registered components on this entity
let serialized_components = component_registry.serialize_entity_components(world, *entity);
for (discriminant, type_path, serialized) in serialized_components {
// Skip networked wrapper components
if type_path.ends_with("::NetworkedEntity") ||
type_path.ends_with("::NetworkedTransform") ||
@@ -485,26 +458,20 @@ fn build_full_state_from_data(
continue;
}
// Try to reflect this component from the entity
if let Some(reflected) = reflect_component.reflect(entity_ref) {
// Serialize the component
if let Ok(serialized) = serialize_component(reflected, type_registry) {
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
match create_component_data(serialized, store) {
| Ok(d) => d,
| Err(_) => continue,
}
} else {
crate::networking::ComponentData::Inline(serialized)
};
components.push(ComponentState {
component_type: type_path.to_string(),
data,
});
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
match create_component_data(serialized, store) {
| Ok(d) => d,
| Err(_) => continue,
}
}
} else {
crate::networking::ComponentData::Inline(serialized)
};
components.push(ComponentState {
discriminant,
data,
});
}
entities.push(EntityState {

View File

@@ -22,7 +22,7 @@ use crate::networking::{
///
/// All messages sent over the network are wrapped in this envelope to support
/// protocol version negotiation and future compatibility.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct VersionedMessage {
/// Protocol version (currently 1)
pub version: u32,
@@ -45,7 +45,7 @@ impl VersionedMessage {
}
/// Join request type - distinguishes fresh joins from rejoin attempts
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub enum JoinType {
/// Fresh join - never connected to this session before
Fresh,
@@ -70,7 +70,7 @@ pub enum JoinType {
/// 2. **Normal Operation**: Peers broadcast `EntityDelta` on changes
/// 3. **Anti-Entropy**: Periodic `SyncRequest` to detect missing operations
/// 4. **Recovery**: `MissingDeltas` sent in response to `SyncRequest`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub enum SyncMessage {
/// Request to join the network and receive full state
///
@@ -156,7 +156,7 @@ pub enum SyncMessage {
/// Complete state of a single entity
///
/// Used in `FullState` messages to transfer all components of an entity.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct EntityState {
/// Network ID of the entity
pub entity_id: uuid::Uuid,
@@ -176,21 +176,20 @@ pub struct EntityState {
/// State of a single component
///
/// Contains the component type and its serialized data.
#[derive(Debug, Clone, Serialize, Deserialize)]
/// Contains the component discriminant and its serialized data.
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct ComponentState {
/// Type path of the component (e.g.,
/// "bevy_transform::components::Transform")
pub component_type: String,
/// Discriminant identifying the component type
pub discriminant: u16,
/// Serialized component data (bincode)
/// Serialized component data (rkyv)
pub data: ComponentData,
}
/// Component data - either inline or a blob reference
///
/// Components larger than 64KB are stored as blobs and referenced by hash.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
pub enum ComponentData {
/// Inline data for small components (<64KB)
Inline(Vec<u8>),
@@ -248,7 +247,7 @@ impl ComponentData {
///
/// This struct exists because EntityDelta is defined as an enum variant
/// but we sometimes need to work with it as a standalone type.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct EntityDelta {
/// Network ID of the entity being updated
pub entity_id: uuid::Uuid,
@@ -343,7 +342,7 @@ mod tests {
}
#[test]
fn test_message_serialization() -> bincode::Result<()> {
fn test_message_serialization() -> anyhow::Result<()> {
let node_id = uuid::Uuid::new_v4();
let session_id = SessionId::new();
let message = SyncMessage::JoinRequest {
@@ -355,8 +354,8 @@ mod tests {
};
let versioned = VersionedMessage::new(message);
let bytes = bincode::serialize(&versioned)?;
let deserialized: VersionedMessage = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&versioned).map(|b| b.to_vec())?;
let deserialized: VersionedMessage = rkyv::from_bytes::<VersionedMessage, rkyv::rancor::Failure>(&bytes)?;
assert_eq!(deserialized.version, versioned.version);
@@ -364,7 +363,7 @@ mod tests {
}
#[test]
fn test_full_state_serialization() -> bincode::Result<()> {
fn test_full_state_serialization() -> anyhow::Result<()> {
let entity_id = uuid::Uuid::new_v4();
let owner_node = uuid::Uuid::new_v4();
@@ -381,8 +380,8 @@ mod tests {
vector_clock: VectorClock::new(),
};
let bytes = bincode::serialize(&message)?;
let _deserialized: SyncMessage = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&message).map(|b| b.to_vec())?;
let _deserialized: SyncMessage = rkyv::from_bytes::<SyncMessage, rkyv::rancor::Failure>(&bytes)?;
Ok(())
}
@@ -392,8 +391,8 @@ mod tests {
let join_type = JoinType::Fresh;
// Fresh join should serialize correctly
let bytes = bincode::serialize(&join_type).unwrap();
let deserialized: JoinType = bincode::deserialize(&bytes).unwrap();
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&join_type).map(|b| b.to_vec()).unwrap();
let deserialized: JoinType = rkyv::from_bytes::<JoinType, rkyv::rancor::Failure>(&bytes).unwrap();
assert!(matches!(deserialized, JoinType::Fresh));
}
@@ -406,8 +405,8 @@ mod tests {
};
// Rejoin should serialize correctly
let bytes = bincode::serialize(&join_type).unwrap();
let deserialized: JoinType = bincode::deserialize(&bytes).unwrap();
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&join_type).map(|b| b.to_vec()).unwrap();
let deserialized: JoinType = rkyv::from_bytes::<JoinType, rkyv::rancor::Failure>(&bytes).unwrap();
match deserialized {
| JoinType::Rejoin {
@@ -434,8 +433,8 @@ mod tests {
join_type: JoinType::Fresh,
};
let bytes = bincode::serialize(&message).unwrap();
let deserialized: SyncMessage = bincode::deserialize(&bytes).unwrap();
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&message).map(|b| b.to_vec()).unwrap();
let deserialized: SyncMessage = rkyv::from_bytes::<SyncMessage, rkyv::rancor::Failure>(&bytes).unwrap();
match deserialized {
| SyncMessage::JoinRequest {
@@ -467,8 +466,8 @@ mod tests {
},
};
let bytes = bincode::serialize(&message).unwrap();
let deserialized: SyncMessage = bincode::deserialize(&bytes).unwrap();
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&message).map(|b| b.to_vec()).unwrap();
let deserialized: SyncMessage = rkyv::from_bytes::<SyncMessage, rkyv::rancor::Failure>(&bytes).unwrap();
match deserialized {
| SyncMessage::JoinRequest {
@@ -484,7 +483,7 @@ mod tests {
}
#[test]
fn test_missing_deltas_serialization() -> bincode::Result<()> {
fn test_missing_deltas_serialization() -> anyhow::Result<()> {
// Test that MissingDeltas message serializes correctly
let node_id = uuid::Uuid::new_v4();
let entity_id = uuid::Uuid::new_v4();
@@ -501,8 +500,8 @@ mod tests {
deltas: vec![delta],
};
let bytes = bincode::serialize(&message)?;
let deserialized: SyncMessage = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&message).map(|b| b.to_vec())?;
let deserialized: SyncMessage = rkyv::from_bytes::<SyncMessage, rkyv::rancor::Failure>(&bytes)?;
match deserialized {
| SyncMessage::MissingDeltas { deltas } => {

View File

@@ -3,75 +3,24 @@
//! This module provides utilities to convert Bevy component changes into
//! ComponentOp operations that can be synchronized across the network.
use bevy::{
prelude::*,
reflect::TypeRegistry,
};
use bevy::prelude::*;
use crate::{
networking::{
blob_support::{
BlobStore,
create_component_data,
},
error::Result,
messages::ComponentData,
operations::{
ComponentOp,
ComponentOpBuilder,
},
vector_clock::{
NodeId,
VectorClock,
},
use crate::networking::{
blob_support::{
BlobStore,
create_component_data,
},
messages::ComponentData,
operations::ComponentOp,
vector_clock::{
NodeId,
VectorClock,
},
persistence::reflection::serialize_component_typed,
};
/// Build a Set operation (LWW) from a component
///
/// Serializes the component using Bevy's reflection system and creates a
/// ComponentOp::Set for Last-Write-Wins synchronization. Automatically uses
/// blob storage for components >64KB.
///
/// # Parameters
///
/// - `component`: The component to serialize
/// - `component_type`: Type path string
/// - `node_id`: Our node ID
/// - `vector_clock`: Current vector clock
/// - `type_registry`: Bevy's type registry
/// - `blob_store`: Optional blob store for large components
///
/// # Returns
///
/// A ComponentOp::Set ready to be broadcast
pub fn build_set_operation(
component: &dyn Reflect,
component_type: String,
node_id: NodeId,
vector_clock: VectorClock,
type_registry: &TypeRegistry,
blob_store: Option<&BlobStore>,
) -> Result<ComponentOp> {
// Serialize the component
let serialized = serialize_component_typed(component, type_registry)?;
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
create_component_data(serialized, store)?
} else {
ComponentData::Inline(serialized)
};
// Build the operation
let builder = ComponentOpBuilder::new(node_id, vector_clock);
Ok(builder.set(component_type, data))
}
/// Build Set operations for all components on an entity
///
/// This iterates over all components with reflection data and creates Set
/// This iterates over all registered Synced components and creates Set
/// operations for each one. Automatically uses blob storage for large
/// components.
///
@@ -81,7 +30,7 @@ pub fn build_set_operation(
/// - `world`: Bevy world
/// - `node_id`: Our node ID
/// - `vector_clock`: Current vector clock
/// - `type_registry`: Bevy's type registry
/// - `type_registry`: Component type registry (for Synced components)
/// - `blob_store`: Optional blob store for large components
///
/// # Returns
@@ -92,64 +41,42 @@ pub fn build_entity_operations(
world: &World,
node_id: NodeId,
vector_clock: VectorClock,
type_registry: &TypeRegistry,
type_registry: &crate::persistence::ComponentTypeRegistry,
blob_store: Option<&BlobStore>,
) -> Vec<ComponentOp> {
let mut operations = Vec::new();
let entity_ref = world.entity(entity);
debug!(
"build_entity_operations: Building operations for entity {:?}",
entity
);
// Iterate over all type registrations
for registration in type_registry.iter() {
// Skip if no ReflectComponent data
let Some(reflect_component) = registration.data::<ReflectComponent>() else {
continue;
// Serialize all Synced components on this entity
let serialized_components = type_registry.serialize_entity_components(world, entity);
for (discriminant, _type_path, serialized) in serialized_components {
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
if let Ok(component_data) = create_component_data(serialized, store) {
component_data
} else {
continue; // Skip this component if blob storage fails
}
} else {
ComponentData::Inline(serialized)
};
// Get the type path
let type_path = registration.type_info().type_path();
// Build the operation
let mut clock = vector_clock.clone();
clock.increment(node_id);
// Skip certain components
if type_path.ends_with("::NetworkedEntity") ||
type_path.ends_with("::NetworkedTransform") ||
type_path.ends_with("::NetworkedSelection") ||
type_path.ends_with("::NetworkedDrawingPath")
{
continue;
}
operations.push(ComponentOp::Set {
discriminant,
data,
vector_clock: clock.clone(),
});
// Try to reflect this component from the entity
if let Some(reflected) = reflect_component.reflect(entity_ref) {
// Serialize the component
if let Ok(serialized) = serialize_component_typed(reflected, type_registry) {
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
if let Ok(component_data) = create_component_data(serialized, store) {
component_data
} else {
continue; // Skip this component if blob storage fails
}
} else {
ComponentData::Inline(serialized)
};
// Build the operation
let mut clock = vector_clock.clone();
clock.increment(node_id);
operations.push(ComponentOp::Set {
component_type: type_path.to_string(),
data,
vector_clock: clock.clone(),
});
debug!(" ✓ Added Set operation for {}", type_path);
}
}
debug!(" ✓ Added Set operation for discriminant {}", discriminant);
}
debug!(
@@ -159,115 +86,3 @@ pub fn build_entity_operations(
);
operations
}
/// Build a Set operation for Transform component specifically
///
/// This is a helper for the common case of synchronizing Transform changes.
///
/// # Example
///
/// ```
/// use bevy::prelude::*;
/// use libmarathon::networking::{
/// VectorClock,
/// build_transform_operation,
/// };
/// use uuid::Uuid;
///
/// # fn example(transform: &Transform, type_registry: &bevy::reflect::TypeRegistry) {
/// let node_id = Uuid::new_v4();
/// let clock = VectorClock::new();
///
/// let op = build_transform_operation(transform, node_id, clock, type_registry, None).unwrap();
/// # }
/// ```
pub fn build_transform_operation(
transform: &Transform,
node_id: NodeId,
vector_clock: VectorClock,
type_registry: &TypeRegistry,
blob_store: Option<&BlobStore>,
) -> Result<ComponentOp> {
// Use reflection to serialize Transform
let serialized = serialize_component_typed(transform.as_reflect(), type_registry)?;
// Create component data (inline or blob)
let data = if let Some(store) = blob_store {
create_component_data(serialized, store)?
} else {
ComponentData::Inline(serialized)
};
let builder = ComponentOpBuilder::new(node_id, vector_clock);
Ok(builder.set(
"bevy_transform::components::transform::Transform".to_string(),
data,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_transform_operation() {
let mut type_registry = TypeRegistry::new();
type_registry.register::<Transform>();
let transform = Transform::default();
let node_id = uuid::Uuid::new_v4();
let clock = VectorClock::new();
let op =
build_transform_operation(&transform, node_id, clock, &type_registry, None).unwrap();
assert!(op.is_set());
assert_eq!(
op.component_type(),
Some("bevy_transform::components::transform::Transform")
);
assert_eq!(op.vector_clock().get(node_id), 1);
}
#[test]
fn test_build_entity_operations() {
let mut world = World::new();
let mut type_registry = TypeRegistry::new();
// Register Transform
type_registry.register::<Transform>();
// Spawn entity with Transform
let entity = world.spawn(Transform::from_xyz(1.0, 2.0, 3.0)).id();
let node_id = uuid::Uuid::new_v4();
let clock = VectorClock::new();
let ops = build_entity_operations(entity, &world, node_id, clock, &type_registry, None);
// Should have at least Transform operation
assert!(!ops.is_empty());
assert!(ops.iter().all(|op| op.is_set()));
}
#[test]
fn test_vector_clock_increment() {
let mut type_registry = TypeRegistry::new();
type_registry.register::<Transform>();
let transform = Transform::default();
let node_id = uuid::Uuid::new_v4();
let mut clock = VectorClock::new();
let op1 =
build_transform_operation(&transform, node_id, clock.clone(), &type_registry, None)
.unwrap();
assert_eq!(op1.vector_clock().get(node_id), 1);
clock.increment(node_id);
let op2 =
build_transform_operation(&transform, node_id, clock.clone(), &type_registry, None)
.unwrap();
assert_eq!(op2.vector_clock().get(node_id), 2);
}
}

View File

@@ -39,7 +39,7 @@ use crate::networking::{
/// - Maintains ordering across concurrent inserts
/// - Uses RGA (Replicated Growable Array) algorithm
/// - Example: Collaborative drawing paths
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub enum ComponentOp {
/// Set a component value (Last-Write-Wins)
///
@@ -50,8 +50,8 @@ pub enum ComponentOp {
/// The data field can be either inline (for small components) or a blob
/// reference (for components >64KB).
Set {
/// Type path of the component
component_type: String,
/// Discriminant identifying the component type
discriminant: u16,
/// Component data (inline or blob reference)
data: ComponentData,
@@ -65,8 +65,8 @@ pub enum ComponentOp {
/// Adds an element to a set that supports concurrent add/remove. Each add
/// has a unique ID so that removes can reference specific adds.
SetAdd {
/// Type path of the component
component_type: String,
/// Discriminant identifying the component type
discriminant: u16,
/// Unique ID for this add operation
operation_id: uuid::Uuid,
@@ -83,8 +83,8 @@ pub enum ComponentOp {
/// Removes an element by referencing the add operation IDs that added it.
/// If concurrent with an add, the add wins (observed-remove semantics).
SetRemove {
/// Type path of the component
component_type: String,
/// Discriminant identifying the component type
discriminant: u16,
/// IDs of the add operations being removed
removed_ids: Vec<uuid::Uuid>,
@@ -99,8 +99,8 @@ pub enum ComponentOp {
/// (Replicated Growable Array) to maintain consistent ordering across
/// concurrent inserts.
SequenceInsert {
/// Type path of the component
component_type: String,
/// Discriminant identifying the component type
discriminant: u16,
/// Unique ID for this insert operation
operation_id: uuid::Uuid,
@@ -120,8 +120,8 @@ pub enum ComponentOp {
/// Marks an element as deleted in the sequence. The element remains in the
/// structure (tombstone) to preserve ordering for concurrent operations.
SequenceDelete {
/// Type path of the component
component_type: String,
/// Discriminant identifying the component type
discriminant: u16,
/// ID of the element to delete
element_id: uuid::Uuid,
@@ -141,14 +141,14 @@ pub enum ComponentOp {
}
impl ComponentOp {
/// Get the component type for this operation
pub fn component_type(&self) -> Option<&str> {
/// Get the component discriminant for this operation
pub fn discriminant(&self) -> Option<u16> {
match self {
| ComponentOp::Set { component_type, .. } |
ComponentOp::SetAdd { component_type, .. } |
ComponentOp::SetRemove { component_type, .. } |
ComponentOp::SequenceInsert { component_type, .. } |
ComponentOp::SequenceDelete { component_type, .. } => Some(component_type),
| ComponentOp::Set { discriminant, .. } |
ComponentOp::SetAdd { discriminant, .. } |
ComponentOp::SetRemove { discriminant, .. } |
ComponentOp::SequenceInsert { discriminant, .. } |
ComponentOp::SequenceDelete { discriminant, .. } => Some(*discriminant),
| ComponentOp::Delete { .. } => None,
}
}
@@ -211,20 +211,20 @@ impl ComponentOpBuilder {
}
/// Build a Set operation (LWW)
pub fn set(mut self, component_type: String, data: ComponentData) -> ComponentOp {
pub fn set(mut self, discriminant: u16, data: ComponentData) -> ComponentOp {
self.vector_clock.increment(self.node_id);
ComponentOp::Set {
component_type,
discriminant,
data,
vector_clock: self.vector_clock,
}
}
/// Build a SetAdd operation (OR-Set)
pub fn set_add(mut self, component_type: String, element: Vec<u8>) -> ComponentOp {
pub fn set_add(mut self, discriminant: u16, element: Vec<u8>) -> ComponentOp {
self.vector_clock.increment(self.node_id);
ComponentOp::SetAdd {
component_type,
discriminant,
operation_id: uuid::Uuid::new_v4(),
element,
vector_clock: self.vector_clock,
@@ -234,12 +234,12 @@ impl ComponentOpBuilder {
/// Build a SetRemove operation (OR-Set)
pub fn set_remove(
mut self,
component_type: String,
discriminant: u16,
removed_ids: Vec<uuid::Uuid>,
) -> ComponentOp {
self.vector_clock.increment(self.node_id);
ComponentOp::SetRemove {
component_type,
discriminant,
removed_ids,
vector_clock: self.vector_clock,
}
@@ -248,13 +248,13 @@ impl ComponentOpBuilder {
/// Build a SequenceInsert operation (RGA)
pub fn sequence_insert(
mut self,
component_type: String,
discriminant: u16,
after_id: Option<uuid::Uuid>,
element: Vec<u8>,
) -> ComponentOp {
self.vector_clock.increment(self.node_id);
ComponentOp::SequenceInsert {
component_type,
discriminant,
operation_id: uuid::Uuid::new_v4(),
after_id,
element,
@@ -265,12 +265,12 @@ impl ComponentOpBuilder {
/// Build a SequenceDelete operation (RGA)
pub fn sequence_delete(
mut self,
component_type: String,
discriminant: u16,
element_id: uuid::Uuid,
) -> ComponentOp {
self.vector_clock.increment(self.node_id);
ComponentOp::SequenceDelete {
component_type,
discriminant,
element_id,
vector_clock: self.vector_clock,
}
@@ -290,29 +290,29 @@ mod tests {
use super::*;
#[test]
fn test_component_type() {
fn test_discriminant() {
let op = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(vec![1, 2, 3]),
vector_clock: VectorClock::new(),
};
assert_eq!(op.component_type(), Some("Transform"));
assert_eq!(op.discriminant(), Some(1));
}
#[test]
fn test_component_type_delete() {
fn test_discriminant_delete() {
let op = ComponentOp::Delete {
vector_clock: VectorClock::new(),
};
assert_eq!(op.component_type(), None);
assert_eq!(op.discriminant(), None);
}
#[test]
fn test_is_set() {
let op = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(vec![1, 2, 3]),
vector_clock: VectorClock::new(),
};
@@ -326,7 +326,7 @@ mod tests {
#[test]
fn test_is_or_set() {
let op = ComponentOp::SetAdd {
component_type: "Selection".to_string(),
discriminant: 2,
operation_id: uuid::Uuid::new_v4(),
element: vec![1, 2, 3],
vector_clock: VectorClock::new(),
@@ -341,7 +341,7 @@ mod tests {
#[test]
fn test_is_sequence() {
let op = ComponentOp::SequenceInsert {
component_type: "DrawingPath".to_string(),
discriminant: 3,
operation_id: uuid::Uuid::new_v4(),
after_id: None,
element: vec![1, 2, 3],
@@ -361,7 +361,7 @@ mod tests {
let builder = ComponentOpBuilder::new(node_id, clock);
let op = builder.set(
"Transform".to_string(),
1,
ComponentData::Inline(vec![1, 2, 3]),
);
@@ -375,22 +375,22 @@ mod tests {
let clock = VectorClock::new();
let builder = ComponentOpBuilder::new(node_id, clock);
let op = builder.set_add("Selection".to_string(), vec![1, 2, 3]);
let op = builder.set_add(2, vec![1, 2, 3]);
assert!(op.is_or_set());
assert_eq!(op.vector_clock().get(node_id), 1);
}
#[test]
fn test_serialization() -> bincode::Result<()> {
fn test_serialization() -> anyhow::Result<()> {
let op = ComponentOp::Set {
component_type: "Transform".to_string(),
discriminant: 1,
data: ComponentData::Inline(vec![1, 2, 3]),
vector_clock: VectorClock::new(),
};
let bytes = bincode::serialize(&op)?;
let deserialized: ComponentOp = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&op).map(|b| b.to_vec())?;
let deserialized: ComponentOp = rkyv::from_bytes::<ComponentOp, rkyv::rancor::Failure>(&bytes)?;
assert!(deserialized.is_set());

View File

@@ -87,7 +87,7 @@ pub struct OrElement<T> {
///
/// An element is "present" if it has an operation ID in `elements` that's
/// not in `tombstones`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct OrSet<T> {
/// Map from operation ID to (value, adding_node)
elements: HashMap<uuid::Uuid, (T, NodeId)>,
@@ -471,15 +471,15 @@ mod tests {
}
#[test]
fn test_orset_serialization() -> bincode::Result<()> {
fn test_orset_serialization() -> anyhow::Result<()> {
let node = uuid::Uuid::new_v4();
let mut set: OrSet<String> = OrSet::new();
set.add("foo".to_string(), node);
set.add("bar".to_string(), node);
let bytes = bincode::serialize(&set)?;
let deserialized: OrSet<String> = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&set).map(|b| b.to_vec())?;
let deserialized: OrSet<String> = rkyv::from_bytes::<OrSet<String>, rkyv::rancor::Failure>(&bytes)?;
assert_eq!(deserialized.len(), 2);
assert!(deserialized.contains(&"foo".to_string()));

View File

@@ -55,7 +55,7 @@ use crate::networking::vector_clock::{
///
/// Each element has a unique ID and tracks its logical position in the sequence
/// via the "after" pointer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct RgaElement<T> {
/// Unique ID for this element
pub id: uuid::Uuid,
@@ -90,7 +90,7 @@ pub struct RgaElement<T> {
/// Elements are stored in a HashMap by ID. Each element tracks which element
/// it was inserted after, forming a linked list structure. Deleted elements
/// remain as tombstones to preserve positions for concurrent operations.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct Rga<T> {
/// Map from element ID to element
elements: HashMap<uuid::Uuid, RgaElement<T>>,
@@ -98,7 +98,7 @@ pub struct Rga<T> {
impl<T> Rga<T>
where
T: Clone + Serialize + for<'de> Deserialize<'de>,
T: Clone + rkyv::Archive,
{
/// Create a new empty RGA sequence
pub fn new() -> Self {
@@ -416,7 +416,7 @@ where
impl<T> Default for Rga<T>
where
T: Clone + Serialize + for<'de> Deserialize<'de>,
T: Clone + rkyv::Archive,
{
fn default() -> Self {
Self::new()
@@ -612,15 +612,15 @@ mod tests {
}
#[test]
fn test_rga_serialization() -> bincode::Result<()> {
fn test_rga_serialization() -> anyhow::Result<()> {
let node = uuid::Uuid::new_v4();
let mut seq: Rga<String> = Rga::new();
let (id_a, _) = seq.insert_at_beginning("foo".to_string(), node);
seq.insert_after(Some(id_a), "bar".to_string(), node);
let bytes = bincode::serialize(&seq)?;
let deserialized: Rga<String> = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&seq).map(|b| b.to_vec())?;
let deserialized: Rga<String> = rkyv::from_bytes::<Rga<String>, rkyv::rancor::Failure>(&bytes)?;
assert_eq!(deserialized.len(), 2);
let values: Vec<String> = deserialized.values().cloned().collect();

View File

@@ -18,7 +18,7 @@ use crate::networking::VectorClock;
///
/// Session IDs provide both technical uniqueness (UUID) and human usability
/// (abc-def-123 codes). All peers in a session share the same session ID.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct SessionId {
uuid: Uuid,
code: String,
@@ -134,7 +134,7 @@ impl fmt::Display for SessionId {
}
/// Session lifecycle states
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub enum SessionState {
/// Session exists in database but hasn't connected to network yet
Created,
@@ -178,7 +178,7 @@ impl SessionState {
///
/// Tracks session identity, creation time, entity count, and lifecycle state.
/// Persisted to database for crash recovery and auto-rejoin.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct Session {
/// Unique session identifier
pub id: SessionId,

View File

@@ -71,12 +71,12 @@ pub trait SyncComponent: Component + Reflect + Sized {
/// Serialize this component to bytes
///
/// Uses bincode for efficient binary serialization.
/// Uses rkyv for zero-copy binary serialization.
fn serialize_sync(&self) -> anyhow::Result<Vec<u8>>;
/// Deserialize this component from bytes
///
/// Uses bincode to deserialize from the format created by `serialize_sync`.
/// Uses rkyv to deserialize from the format created by `serialize_sync`.
fn deserialize_sync(data: &[u8]) -> anyhow::Result<Self>;
/// Merge remote state with local state

View File

@@ -54,7 +54,7 @@ pub type NodeId = uuid::Uuid;
/// clock1.merge(&clock2); // node1: 1, node2: 1
/// assert!(clock1.happened_before(&clock2) == false);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Default)]
pub struct VectorClock {
/// Map from node ID to logical timestamp
pub clocks: HashMap<NodeId, u64>,
@@ -444,13 +444,13 @@ mod tests {
}
#[test]
fn test_serialization() -> bincode::Result<()> {
fn test_serialization() -> anyhow::Result<()> {
let node = uuid::Uuid::new_v4();
let mut clock = VectorClock::new();
clock.increment(node);
let bytes = bincode::serialize(&clock)?;
let deserialized: VectorClock = bincode::deserialize(&bytes)?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Failure>(&clock).map(|b| b.to_vec())?;
let deserialized: VectorClock = rkyv::from_bytes::<VectorClock, rkyv::rancor::Failure>(&bytes)?;
assert_eq!(clock, deserialized);