1use std::collections::HashMap;
4
5use crate::entity::{DirtyMask, EntityRecord, EntityRole, EntityTags};
6use crate::handoff::HandoffTransfer;
7use crate::ids::{
8 EntityHandle, EntityId, InstanceId, NodeId, OwnerEpoch, PolicyId, StationId, Tick,
9};
10use crate::snapshot::{SnapshotMeta, SnapshotVersion, StationSnapshot};
11use crate::spatial::{Bounds, Position3};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct StationConfig {
16 pub station_id: StationId,
18 pub node_id: NodeId,
20 pub instance_id: InstanceId,
22 pub tick_rate_hz: u16,
24}
25
26#[derive(Clone, Debug)]
28pub struct Station {
29 config: StationConfig,
30 tick: Tick,
31 owner_epoch: OwnerEpoch,
32 records: Vec<Option<EntityRecord>>,
33 generations: Vec<u32>,
34 free: Vec<u32>,
35 by_id: HashMap<EntityId, EntityHandle>,
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub struct StationRestoreStats {
41 pub snapshot_entities: usize,
43 pub initial_entity_capacity: usize,
45 pub initial_id_index_capacity: usize,
47 pub final_entity_capacity: usize,
49 pub final_id_index_capacity: usize,
51 pub entity_capacity_grew: bool,
53 pub id_index_capacity_grew: bool,
55}
56
57impl Station {
58 pub fn new(config: StationConfig) -> Self {
60 Self::with_capacity(config, 0)
61 }
62
63 pub fn with_capacity(config: StationConfig, entity_capacity: usize) -> Self {
65 Self {
66 config,
67 tick: Tick::new(0),
68 owner_epoch: OwnerEpoch::new(0),
69 records: Vec::with_capacity(entity_capacity),
70 generations: Vec::with_capacity(entity_capacity),
71 free: Vec::new(),
72 by_id: HashMap::with_capacity(entity_capacity),
73 }
74 }
75
76 pub fn reserve_entities(&mut self, additional: usize) {
78 self.records.reserve(additional);
79 self.generations.reserve(additional);
80 self.by_id.reserve(additional);
81 }
82
83 pub fn reserve_free_handles(&mut self, additional: usize) {
85 self.free.reserve(additional);
86 }
87
88 pub fn entity_capacity(&self) -> usize {
90 self.records.capacity().min(self.generations.capacity())
91 }
92
93 pub fn id_index_capacity(&self) -> usize {
95 self.by_id.capacity()
96 }
97
98 pub fn free_list_capacity(&self) -> usize {
100 self.free.capacity()
101 }
102
103 pub const fn config(&self) -> StationConfig {
105 self.config
106 }
107
108 pub const fn tick(&self) -> Tick {
110 self.tick
111 }
112
113 pub const fn owner_epoch(&self) -> OwnerEpoch {
115 self.owner_epoch
116 }
117
118 pub fn next_owner_epoch(&mut self) -> OwnerEpoch {
120 self.owner_epoch = OwnerEpoch::new(self.owner_epoch.get().saturating_add(1));
121 self.owner_epoch
122 }
123
124 pub fn len(&self) -> usize {
126 self.by_id.len()
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.by_id.is_empty()
132 }
133
134 pub fn advance_tick(&mut self) {
136 self.tick = Tick::new(self.tick.get().saturating_add(1));
137 }
138
139 pub fn spawn_owned(
141 &mut self,
142 id: EntityId,
143 position: Position3,
144 bounds: Bounds,
145 policy_id: PolicyId,
146 ) -> Result<EntityHandle, StationError> {
147 if self.by_id.contains_key(&id) {
148 return Err(StationError::DuplicateEntity(id));
149 }
150
151 let handle = self.allocate_handle();
152 let record = EntityRecord::owned(id, handle, position, bounds, policy_id, self.owner_epoch);
153 self.insert_allocated(handle, record);
154 Ok(handle)
155 }
156
157 #[allow(clippy::too_many_arguments)]
159 pub fn upsert_ghost(
160 &mut self,
161 id: EntityId,
162 position: Position3,
163 bounds: Bounds,
164 policy_id: PolicyId,
165 owner_station: StationId,
166 owner_epoch: OwnerEpoch,
167 expires_at: Tick,
168 ) -> EntityHandle {
169 if let Some(handle) = self.by_id.get(&id).copied() {
170 if let Some(record) = self.get_mut(handle) {
171 record.position = position;
172 record.bounds = bounds;
173 record.policy_id = policy_id;
174 record.role = EntityRole::Ghost {
175 owner_station,
176 owner_epoch,
177 expires_at,
178 };
179 record.dirty.insert(DirtyMask::TRANSFORM);
180 }
181 return handle;
182 }
183
184 let handle = self.allocate_handle();
185 let record = EntityRecord::ghost(
186 id,
187 handle,
188 position,
189 bounds,
190 policy_id,
191 owner_station,
192 owner_epoch,
193 expires_at,
194 );
195 self.insert_allocated(handle, record);
196 handle
197 }
198
199 pub fn get(&self, handle: EntityHandle) -> Option<&EntityRecord> {
201 let index = usize::try_from(handle.index()).ok()?;
202 let generation = self.generations.get(index).copied()?;
203 if generation != handle.generation() {
204 return None;
205 }
206 self.records.get(index)?.as_ref()
207 }
208
209 pub fn get_mut(&mut self, handle: EntityHandle) -> Option<&mut EntityRecord> {
211 let index = usize::try_from(handle.index()).ok()?;
212 let generation = self.generations.get(index).copied()?;
213 if generation != handle.generation() {
214 return None;
215 }
216 self.records.get_mut(index)?.as_mut()
217 }
218
219 pub fn get_by_id(&self, id: EntityId) -> Option<&EntityRecord> {
221 let handle = self.by_id.get(&id).copied()?;
222 self.get(handle)
223 }
224
225 pub fn get_by_id_mut(&mut self, id: EntityId) -> Option<&mut EntityRecord> {
227 let handle = self.by_id.get(&id).copied()?;
228 self.get_mut(handle)
229 }
230
231 pub fn handle_by_id(&self, id: EntityId) -> Option<EntityHandle> {
233 self.by_id.get(&id).copied()
234 }
235
236 pub fn move_owned(
238 &mut self,
239 handle: EntityHandle,
240 position: Position3,
241 ) -> Result<(), StationError> {
242 let record = self
243 .get_mut(handle)
244 .ok_or(StationError::MissingEntityHandle(handle))?;
245 if !record.is_owned() {
246 return Err(StationError::NotOwner(record.id));
247 }
248 record.position = position;
249 record.dirty.insert(DirtyMask::TRANSFORM);
250 Ok(())
251 }
252
253 pub fn set_tags(&mut self, handle: EntityHandle, tags: EntityTags) -> Result<(), StationError> {
255 let record = self
256 .get_mut(handle)
257 .ok_or(StationError::MissingEntityHandle(handle))?;
258 if !record.is_owned() {
259 return Err(StationError::NotOwner(record.id));
260 }
261 record.tags = tags;
262 record.dirty.insert(DirtyMask::TAGS);
263 Ok(())
264 }
265
266 pub fn clear_dirty(
268 &mut self,
269 handle: EntityHandle,
270 mask: DirtyMask,
271 ) -> Result<(), StationError> {
272 let record = self
273 .get_mut(handle)
274 .ok_or(StationError::MissingEntityHandle(handle))?;
275 record.dirty.remove(mask);
276 Ok(())
277 }
278
279 pub fn iter(&self) -> impl Iterator<Item = &EntityRecord> {
281 self.records.iter().filter_map(Option::as_ref)
282 }
283
284 pub fn remove(&mut self, handle: EntityHandle) -> Result<EntityRecord, StationError> {
286 let index = usize::try_from(handle.index())
287 .map_err(|_| StationError::MissingEntityHandle(handle))?;
288 let generation = self
289 .generations
290 .get(index)
291 .copied()
292 .ok_or(StationError::MissingEntityHandle(handle))?;
293 if generation != handle.generation() {
294 return Err(StationError::MissingEntityHandle(handle));
295 }
296
297 let record = self.records[index]
298 .take()
299 .ok_or(StationError::MissingEntityHandle(handle))?;
300 self.by_id.remove(&record.id);
301 self.generations[index] = self.generations[index].saturating_add(1);
302 self.free.push(handle.index());
303 Ok(record)
304 }
305
306 pub fn remove_by_id(&mut self, id: EntityId) -> Result<EntityRecord, StationError> {
308 let handle = self
309 .by_id
310 .get(&id)
311 .copied()
312 .ok_or(StationError::MissingEntity(id))?;
313 self.remove(handle)
314 }
315
316 pub fn prepare_outgoing_handoff(
318 &self,
319 entity_id: EntityId,
320 target_station: StationId,
321 target_owner_epoch: OwnerEpoch,
322 source_ghost_expires_at: Tick,
323 ) -> Result<HandoffTransfer, StationError> {
324 if target_station == self.config.station_id {
325 return Err(StationError::HandoffTargetIsSource(target_station));
326 }
327
328 let entity = self
329 .get_by_id(entity_id)
330 .ok_or(StationError::MissingEntity(entity_id))?;
331 if !entity.is_owned() {
332 return Err(StationError::NotOwner(entity_id));
333 }
334
335 Ok(HandoffTransfer {
336 entity_id,
337 source_station: self.config.station_id,
338 target_station,
339 source_owner_epoch: entity.role.owner_epoch(),
340 target_owner_epoch,
341 prepared_at: self.tick,
342 source_ghost_expires_at,
343 entity: entity.clone(),
344 })
345 }
346
347 pub fn prewarm_handoff_ghost(
349 &mut self,
350 transfer: &HandoffTransfer,
351 ) -> Result<EntityHandle, StationError> {
352 if transfer.target_station != self.config.station_id {
353 return Err(StationError::WrongHandoffTarget {
354 expected: self.config.station_id,
355 actual: transfer.target_station,
356 });
357 }
358
359 Ok(self.upsert_ghost(
360 transfer.entity_id,
361 transfer.entity.position,
362 transfer.entity.bounds,
363 transfer.entity.policy_id,
364 transfer.source_station,
365 transfer.source_owner_epoch,
366 transfer.source_ghost_expires_at,
367 ))
368 }
369
370 pub fn commit_incoming_handoff(
372 &mut self,
373 transfer: HandoffTransfer,
374 ) -> Result<EntityHandle, StationError> {
375 if transfer.target_station != self.config.station_id {
376 return Err(StationError::WrongHandoffTarget {
377 expected: self.config.station_id,
378 actual: transfer.target_station,
379 });
380 }
381
382 if let Some(handle) = self.handle_by_id(transfer.entity_id) {
383 let record = self
384 .get_mut(handle)
385 .ok_or(StationError::MissingEntityHandle(handle))?;
386 if record.is_owned() {
387 return Err(StationError::AlreadyOwner(transfer.entity_id));
388 }
389
390 *record = transfer.entity;
391 record.handle = handle;
392 record.role = EntityRole::Owned {
393 owner_epoch: transfer.target_owner_epoch,
394 };
395 record.dirty.insert(DirtyMask::TRANSFORM);
396 self.owner_epoch = transfer.target_owner_epoch;
397 return Ok(handle);
398 }
399
400 let handle = self.allocate_handle();
401 let mut record = transfer.entity;
402 record.handle = handle;
403 record.role = EntityRole::Owned {
404 owner_epoch: transfer.target_owner_epoch,
405 };
406 record.dirty.insert(DirtyMask::TRANSFORM);
407 self.owner_epoch = transfer.target_owner_epoch;
408 self.insert_allocated(handle, record);
409 Ok(handle)
410 }
411
412 pub fn commit_outgoing_handoff(
414 &mut self,
415 transfer: &HandoffTransfer,
416 ) -> Result<EntityHandle, StationError> {
417 if transfer.source_station != self.config.station_id {
418 return Err(StationError::WrongHandoffSource {
419 expected: self.config.station_id,
420 actual: transfer.source_station,
421 });
422 }
423
424 let handle = self
425 .handle_by_id(transfer.entity_id)
426 .ok_or(StationError::MissingEntity(transfer.entity_id))?;
427 let record = self
428 .get_mut(handle)
429 .ok_or(StationError::MissingEntityHandle(handle))?;
430 if !record.is_owned() {
431 return Err(StationError::NotOwner(transfer.entity_id));
432 }
433
434 record.role = EntityRole::Ghost {
435 owner_station: transfer.target_station,
436 owner_epoch: transfer.target_owner_epoch,
437 expires_at: transfer.source_ghost_expires_at,
438 };
439 record.dirty.insert(DirtyMask::TRANSFORM);
440 Ok(handle)
441 }
442
443 pub fn snapshot(&self, version: SnapshotVersion) -> StationSnapshot {
445 let mut snapshot = StationSnapshot::default();
446 self.snapshot_into(version, &mut snapshot);
447 snapshot
448 }
449
450 pub fn snapshot_into(&self, version: SnapshotVersion, snapshot: &mut StationSnapshot) {
452 snapshot.entities.clear();
453 snapshot.entities.extend(self.iter().cloned());
454 snapshot.meta = SnapshotMeta {
455 instance_id: self.config.instance_id,
456 station_id: self.config.station_id,
457 tick: self.tick,
458 entity_count: snapshot.entities.len(),
459 owner_epoch: self.owner_epoch,
460 version,
461 };
462 }
463
464 pub fn restore(config: StationConfig, snapshot: StationSnapshot) -> Result<Self, StationError> {
466 Self::restore_tracked(config, snapshot).map(|(station, _)| station)
467 }
468
469 pub fn restore_tracked(
471 config: StationConfig,
472 snapshot: StationSnapshot,
473 ) -> Result<(Self, StationRestoreStats), StationError> {
474 if snapshot.meta.station_id != config.station_id {
475 return Err(StationError::SnapshotStationMismatch {
476 expected: config.station_id,
477 actual: snapshot.meta.station_id,
478 });
479 }
480
481 let entity_count = snapshot.entities.len();
482 let mut station = Self::with_capacity(config, entity_count);
483 let initial_entity_capacity = station.entity_capacity();
484 let initial_id_index_capacity = station.id_index_capacity();
485 station.tick = snapshot.meta.tick;
486 station.owner_epoch = snapshot.meta.owner_epoch;
487
488 for mut record in snapshot.entities {
489 if station.by_id.contains_key(&record.id) {
490 return Err(StationError::DuplicateEntity(record.id));
491 }
492 let handle = station.allocate_handle();
493 record.handle = handle;
494 station.insert_allocated(handle, record);
495 }
496
497 let stats = StationRestoreStats {
498 snapshot_entities: entity_count,
499 initial_entity_capacity,
500 initial_id_index_capacity,
501 final_entity_capacity: station.entity_capacity(),
502 final_id_index_capacity: station.id_index_capacity(),
503 entity_capacity_grew: station.entity_capacity() > initial_entity_capacity,
504 id_index_capacity_grew: station.id_index_capacity() > initial_id_index_capacity,
505 };
506 Ok((station, stats))
507 }
508
509 fn allocate_handle(&mut self) -> EntityHandle {
510 if let Some(index) = self.free.pop() {
511 let generation = self.generations[index as usize];
512 EntityHandle::new(index, generation)
513 } else {
514 let index =
515 u32::try_from(self.records.len()).expect("station entity capacity exceeded");
516 self.records.push(None);
517 self.generations.push(0);
518 EntityHandle::new(index, 0)
519 }
520 }
521
522 fn insert_allocated(&mut self, handle: EntityHandle, record: EntityRecord) {
523 let index = handle.index() as usize;
524 self.by_id.insert(record.id, handle);
525 self.records[index] = Some(record);
526 }
527}
528
529#[derive(Clone, Copy, Debug, PartialEq, Eq)]
531pub enum StationError {
532 DuplicateEntity(EntityId),
534 MissingEntity(EntityId),
536 MissingEntityHandle(EntityHandle),
538 NotOwner(EntityId),
540 AlreadyOwner(EntityId),
542 HandoffTargetIsSource(StationId),
544 WrongHandoffTarget {
546 expected: StationId,
548 actual: StationId,
550 },
551 WrongHandoffSource {
553 expected: StationId,
555 actual: StationId,
557 },
558 SnapshotStationMismatch {
560 expected: StationId,
562 actual: StationId,
564 },
565}
566
567impl core::fmt::Display for StationError {
568 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
569 match self {
570 Self::DuplicateEntity(id) => write!(f, "duplicate entity id {}", id.get()),
571 Self::MissingEntity(id) => write!(f, "missing entity id {}", id.get()),
572 Self::MissingEntityHandle(handle) => {
573 write!(
574 f,
575 "missing entity handle index={} generation={}",
576 handle.index(),
577 handle.generation()
578 )
579 }
580 Self::NotOwner(id) => write!(f, "entity {} is not authoritative here", id.get()),
581 Self::AlreadyOwner(id) => {
582 write!(f, "entity {} is already authoritative here", id.get())
583 }
584 Self::HandoffTargetIsSource(station_id) => {
585 write!(
586 f,
587 "handoff target {} is the source station",
588 station_id.get()
589 )
590 }
591 Self::WrongHandoffTarget { expected, actual } => write!(
592 f,
593 "wrong handoff target: expected {}, got {}",
594 expected.get(),
595 actual.get()
596 ),
597 Self::WrongHandoffSource { expected, actual } => write!(
598 f,
599 "wrong handoff source: expected {}, got {}",
600 expected.get(),
601 actual.get()
602 ),
603 Self::SnapshotStationMismatch { expected, actual } => write!(
604 f,
605 "snapshot station mismatch: expected {}, got {}",
606 expected.get(),
607 actual.get()
608 ),
609 }
610 }
611}
612
613impl std::error::Error for StationError {}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 fn config() -> StationConfig {
620 StationConfig {
621 station_id: StationId::new(1),
622 node_id: NodeId::new(1),
623 instance_id: InstanceId::new(7),
624 tick_rate_hz: 20,
625 }
626 }
627
628 fn config_with_station(station_id: u32) -> StationConfig {
629 StationConfig {
630 station_id: StationId::new(station_id),
631 node_id: NodeId::new(1),
632 instance_id: InstanceId::new(7),
633 tick_rate_hz: 20,
634 }
635 }
636
637 #[test]
638 fn explicit_entity_capacity_is_retained_and_grows_on_request() {
639 let mut station = Station::with_capacity(config(), 8);
640
641 assert!(station.entity_capacity() >= 8);
642 assert!(station.id_index_capacity() >= 8);
643 assert_eq!(station.free_list_capacity(), 0);
644
645 station.reserve_entities(32);
646 assert!(station.entity_capacity() >= 32);
647 assert!(station.id_index_capacity() >= 32);
648 assert_eq!(station.free_list_capacity(), 0);
649 station.reserve_free_handles(8);
650 assert!(station.free_list_capacity() >= 8);
651
652 let handle = station
653 .spawn_owned(
654 EntityId::new(1),
655 Position3::new(0.0, 0.0, 0.0),
656 Bounds::Point,
657 PolicyId::new(1),
658 )
659 .expect("reserved station should spawn");
660 assert_eq!(handle, EntityHandle::new(0, 0));
661 }
662
663 #[test]
664 fn owned_entities_can_move_and_snapshot_restore() {
665 let mut station = Station::new(config());
666 let handle = station
667 .spawn_owned(
668 EntityId::new(42),
669 Position3::new(1.0, 2.0, 3.0),
670 Bounds::Point,
671 PolicyId::new(0),
672 )
673 .expect("spawn should work");
674
675 station
676 .move_owned(handle, Position3::new(2.0, 3.0, 4.0))
677 .expect("owned move should work");
678 station.advance_tick();
679
680 let version = SnapshotVersion::default();
681 let snapshot = station.snapshot(version);
682 let mut reusable = StationSnapshot::default();
683 reusable.entities.reserve(1);
684 station.snapshot_into(version, &mut reusable);
685 assert_eq!(reusable, snapshot);
686 let retained_pointer = reusable.entities.as_ptr();
687 let retained_capacity = reusable.entities.capacity();
688 station.snapshot_into(version, &mut reusable);
689 assert_eq!(reusable, snapshot);
690 assert_eq!(reusable.entities.as_ptr(), retained_pointer);
691 assert_eq!(reusable.entities.capacity(), retained_capacity);
692 let (restored, restore_stats) =
693 Station::restore_tracked(config(), snapshot).expect("restore should work");
694
695 assert_eq!(restored.len(), 1);
696 assert!(restored.entity_capacity() >= 1);
697 assert!(restored.id_index_capacity() >= 1);
698 assert_eq!(restore_stats.snapshot_entities, 1);
699 assert!(restore_stats.initial_entity_capacity >= 1);
700 assert!(restore_stats.initial_id_index_capacity >= 1);
701 assert!(!restore_stats.entity_capacity_grew);
702 assert!(!restore_stats.id_index_capacity_grew);
703 assert_eq!(restored.tick(), Tick::new(1));
704 assert_eq!(
705 restored
706 .get_by_id(EntityId::new(42))
707 .expect("entity should exist")
708 .position,
709 Position3::new(2.0, 3.0, 4.0)
710 );
711 }
712
713 #[test]
714 fn ghosts_cannot_be_moved_as_owned() {
715 let mut station = Station::new(config());
716 let handle = station.upsert_ghost(
717 EntityId::new(5),
718 Position3::new(0.0, 0.0, 0.0),
719 Bounds::Point,
720 PolicyId::new(0),
721 StationId::new(2),
722 OwnerEpoch::new(1),
723 Tick::new(10),
724 );
725
726 let error = station
727 .move_owned(handle, Position3::new(1.0, 0.0, 0.0))
728 .expect_err("ghost move should fail");
729 assert_eq!(error, StationError::NotOwner(EntityId::new(5)));
730 }
731
732 #[test]
733 fn owned_tags_can_be_replaced_and_mark_dirty() {
734 let mut station = Station::new(config());
735 let handle = station
736 .spawn_owned(
737 EntityId::new(6),
738 Position3::new(0.0, 0.0, 0.0),
739 Bounds::Point,
740 PolicyId::new(0),
741 )
742 .expect("spawn should work");
743 let tags = EntityTags::from_bits(0b101);
744
745 station
746 .set_tags(handle, tags)
747 .expect("set tags should work");
748 let record = station.get(handle).expect("entity should exist");
749
750 assert_eq!(record.tags, tags);
751 assert!(record.dirty.contains(DirtyMask::TAGS));
752 }
753
754 #[test]
755 fn selected_dirty_bits_can_be_cleared() {
756 let mut station = Station::new(config());
757 let handle = station
758 .spawn_owned(
759 EntityId::new(8),
760 Position3::new(0.0, 0.0, 0.0),
761 Bounds::Point,
762 PolicyId::new(0),
763 )
764 .expect("spawn should work");
765 station
766 .set_tags(handle, EntityTags::from_bits(1))
767 .expect("set tags should work");
768
769 station
770 .clear_dirty(handle, DirtyMask::TAGS)
771 .expect("clear dirty should work");
772 let record = station.get(handle).expect("entity should exist");
773
774 assert!(!record.dirty.contains(DirtyMask::TAGS));
775 assert!(record.dirty.contains(DirtyMask::TRANSFORM));
776 }
777
778 #[test]
779 fn ghost_tags_cannot_be_replaced_as_owned() {
780 let mut station = Station::new(config());
781 let handle = station.upsert_ghost(
782 EntityId::new(7),
783 Position3::new(0.0, 0.0, 0.0),
784 Bounds::Point,
785 PolicyId::new(0),
786 StationId::new(2),
787 OwnerEpoch::new(1),
788 Tick::new(10),
789 );
790
791 let error = station
792 .set_tags(handle, EntityTags::from_bits(1))
793 .expect_err("ghost tags should fail");
794 assert_eq!(error, StationError::NotOwner(EntityId::new(7)));
795 }
796
797 #[test]
798 fn two_phase_handoff_prewarms_target_and_commits_owner_switch() {
799 let mut source = Station::new(config_with_station(1));
800 let mut target = Station::new(config_with_station(2));
801
802 source
803 .spawn_owned(
804 EntityId::new(9),
805 Position3::new(10.0, 20.0, 30.0),
806 Bounds::Point,
807 PolicyId::new(0),
808 )
809 .expect("spawn should work");
810 source.advance_tick();
811
812 let target_epoch = target.next_owner_epoch();
813 let transfer = source
814 .prepare_outgoing_handoff(
815 EntityId::new(9),
816 StationId::new(2),
817 target_epoch,
818 Tick::new(8),
819 )
820 .expect("prepare should work");
821
822 let ghost_handle = target
823 .prewarm_handoff_ghost(&transfer)
824 .expect("prewarm should work");
825 assert!(!target.get(ghost_handle).expect("ghost exists").is_owned());
826
827 let owner_handle = target
828 .commit_incoming_handoff(transfer.clone())
829 .expect("incoming commit should work");
830 assert!(target.get(owner_handle).expect("owner exists").is_owned());
831
832 let source_handle = source
833 .commit_outgoing_handoff(&transfer)
834 .expect("outgoing commit should work");
835 let source_record = source.get(source_handle).expect("source ghost exists");
836 assert!(!source_record.is_owned());
837 assert_eq!(source_record.role.owner_epoch(), target_epoch);
838 }
839}