aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-world
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 15:02:16 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 15:02:16 +0300
commitbe41fa839fe99f152d26048675b290599492f16b (patch)
treebb57c404b192adc1058043337a2558b49f6fb0e2 /crates/fparkan-world
parent8e5e46b7b381608387fcd2fdd98a474a50f3d33a (diff)
downloadfparkan-be41fa839fe99f152d26048675b290599492f16b.tar.xz
fparkan-be41fa839fe99f152d26048675b290599492f16b.zip
fix: harden resource and world state correctness
Diffstat (limited to 'crates/fparkan-world')
-rw-r--r--crates/fparkan-world/src/lib.rs97
1 files changed, 72 insertions, 25 deletions
diff --git a/crates/fparkan-world/src/lib.rs b/crates/fparkan-world/src/lib.rs
index 58412d9..a253586 100644
--- a/crates/fparkan-world/src/lib.rs
+++ b/crates/fparkan-world/src/lib.rs
@@ -357,36 +357,45 @@ pub fn step_with_handler<F>(
where
F: FnMut(&mut World, &WorldCommand) -> Result<(), WorldError>,
{
+ let before = world.clone();
world.phase = WorldPhase::Calculating;
let mut events = Vec::new();
- while let Some(command) = world.queue.pop_front() {
- if let Some(handle) = command.target {
- if world.deferred_delete.contains(&handle) {
- continue;
+ let result = (|| {
+ while let Some(command) = world.queue.pop_front() {
+ if let Some(handle) = command.target {
+ if world.deferred_delete.contains(&handle) {
+ continue;
+ }
+ checked_slot(world, handle)?;
}
- checked_slot(world, handle)?;
+ handler(world, &command)?;
+ events.push(WorldEvent {
+ sequence: command.sequence,
+ target: command.target,
+ });
}
- handler(world, &command)?;
- events.push(WorldEvent {
- sequence: command.sequence,
- target: command.target,
- });
- }
- world.phase = WorldPhase::ApplyingDeferred;
- let deletes = std::mem::take(&mut world.deferred_delete);
- for handle in deletes {
- let _ = delete_now(world, handle);
+ world.phase = WorldPhase::ApplyingDeferred;
+ let deletes = std::mem::take(&mut world.deferred_delete);
+ for handle in deletes {
+ delete_now(world, handle)?;
+ }
+ world.tick.0 = world.tick.0.saturating_add(1);
+ world.phase = WorldPhase::PublishingSnapshot;
+ let snapshot = WorldSnapshot {
+ tick: world.tick,
+ objects: live_registered(world),
+ events,
+ hash: canonical_state_hash(world),
+ };
+ world.phase = WorldPhase::Idle;
+ Ok(snapshot)
+ })();
+ if let Err(err) = result {
+ *world = before;
+ world.phase = WorldPhase::Idle;
+ return Err(err);
}
- world.tick.0 = world.tick.0.saturating_add(1);
- world.phase = WorldPhase::PublishingSnapshot;
- let snapshot = WorldSnapshot {
- tick: world.tick,
- objects: live_registered(world),
- events,
- hash: canonical_state_hash(world),
- };
- world.phase = WorldPhase::Idle;
- Ok(snapshot)
+ result
}
/// Computes canonical state hash.
@@ -711,6 +720,44 @@ mod tests {
}
#[test]
+ fn callback_error_rolls_back_phase_queue_and_deferred_deletes() {
+ let mut world = new(WorldConfig);
+ let first = construct_object(&mut world, ObjectDraft { original_id: None }).expect("first");
+ register_object(&mut world, first).expect("register");
+ enqueue(
+ &mut world,
+ WorldCommand {
+ sequence: 7,
+ target: Some(first),
+ },
+ )
+ .expect("enqueue");
+
+ let err = step_with_handler(&mut world, &InputSnapshot, |world, _| {
+ request_delete(world, first)?;
+ Err(WorldError::InvalidFixedStep)
+ })
+ .expect_err("handler error");
+
+ assert_eq!(err, WorldError::InvalidFixedStep);
+ assert_eq!(world.phase, WorldPhase::Idle);
+ assert_eq!(world.tick, Tick(0));
+ assert!(world.deferred_delete.is_empty());
+ assert_eq!(world.queue.len(), 1);
+
+ let snapshot = step(&mut world, &InputSnapshot).expect("retry step");
+ assert_eq!(snapshot.tick, Tick(1));
+ assert_eq!(
+ snapshot.events,
+ vec![WorldEvent {
+ sequence: 0,
+ target: Some(first)
+ }]
+ );
+ assert_eq!(snapshot.objects, vec![first]);
+ }
+
+ #[test]
fn snapshot_hash_determinism_and_immutability() {
let mut left = new(WorldConfig);
let mut right = new(WorldConfig);