Skip to main content

rig_ecs/checkpoint/
restore.rs

1//! Validate checkpoint requirements and install host-constructed handlers.
2//!
3//! ```
4//! use rig_ecs::{RigPlugin, checkpoint::{Checkpoint, RestoreMode, load_world}};
5//! let mut app = bevy_app::App::new();
6//! app.add_plugins(RigPlugin::default());
7//! load_world(&Checkpoint::default(), app.world_mut(), RestoreMode::Strict, [])?;
8//! # Ok::<(), rig_ecs::checkpoint::CheckpointError>(())
9//! ```
10
11use std::collections::{BTreeMap, HashSet};
12
13use bevy_ecs::prelude::*;
14use bevy_reflect::TypePath;
15use rig_core::{
16    effect::{HandlerDescriptor, HandlerKey},
17    serve::ErasedHandler,
18};
19
20use super::{Checkpoint, CheckpointError, Loaded};
21use crate::bus::{Bound, HandlerIndex, handlers::HandlerTable};
22
23/// How the host authorizes the implementations serving a resumed checkpoint.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RestoreMode {
26    /// Every implementation must match the original saved descriptor after
27    /// supplied-key normalization (see [`load_world`]). Supplied
28    /// handlers replace matching installed handlers, including credential rotation;
29    /// omitted handlers keep the matching installed implementation. A supplied
30    /// task may replace a world/system implementation with the same descriptor;
31    /// the descriptor does not attest implementation kind.
32    Strict,
33    /// Explicitly accept changed descriptors within the same effect family.
34    /// Supplied handlers replace installed implementations; omitted keys use the
35    /// destination's installed implementation. This is not strict restoration.
36    Replace,
37}
38
39impl Checkpoint {
40    /// Return original advertised contracts in key order, before destination aliasing.
41    /// Returns an error for malformed descriptors, inconsistent keys, or duplicates;
42    /// performs no credential lookup or provider construction.
43    pub fn requirements(&self) -> Result<Vec<HandlerDescriptor>, CheckpointError> {
44        let mut keys = HashSet::new();
45        let mut requirements = Vec::new();
46        for (row, entity) in self.entities.iter().enumerate() {
47            if let Some(value) = entity.get(Bound::type_path()) {
48                let bound = super::saved_bound(row, value)?;
49                if bound.key != bound.descriptor.key {
50                    return Err(CheckpointError::HandlerKeyMismatch { key: bound.key });
51                }
52                if !keys.insert(bound.key.clone()) {
53                    return Err(CheckpointError::DuplicateSavedHandler { key: bound.key });
54                }
55                requirements.push(bound.descriptor);
56            }
57        }
58        requirements.sort_by(|a, b| a.key.cmp(&b.key));
59        Ok(requirements)
60    }
61}
62
63/// Validate and load execution state, returning entities in checkpoint order.
64/// Returns an error for invalid state, incomplete or incompatible handlers,
65/// duplicate supplied keys, or keys absent from the saved requirements.
66/// Validation refusal leaves destination state and handlers unchanged.
67///
68/// Supplied handlers replace installed implementations even when descriptors
69/// match; omitted keys retain installed handlers, including world/system handlers.
70/// Supplied descriptor keys are normalized to their pair keys. Strict mode compares
71/// all remaining fields against the original saved contracts before aliasing;
72/// saved bound and descriptor keys must agree. Both modes require matching families.
73///
74/// Hosts must construct endpoints, credentials, and transport policy correctly;
75/// descriptors do not attest them. Register insertion observers after loading,
76/// since observers are not transactional. Running destination operations retain
77/// their captured handlers.
78pub fn load_world(
79    checkpoint: &Checkpoint,
80    world: &mut World,
81    mode: RestoreMode,
82    handlers: impl IntoIterator<Item = (HandlerKey, ErasedHandler)>,
83) -> Result<Loaded, CheckpointError> {
84    let requirements = checkpoint.requirements()?;
85    let (assets, aliases) = super::validated_state(checkpoint, world)?;
86    let mut supplied = BTreeMap::new();
87    for (key, handler) in handlers {
88        if supplied.insert(key.clone(), handler).is_some() {
89            return Err(CheckpointError::DuplicateSuppliedHandler { key });
90        }
91    }
92    let index = world
93        .get_resource::<HandlerIndex>()
94        .ok_or(CheckpointError::NotInstalled("handler index"))?;
95    let table = world
96        .get_non_send::<HandlerTable>()
97        .ok_or(CheckpointError::NotInstalled("handler table"))?;
98    let mut descriptors = BTreeMap::new();
99    for saved in &requirements {
100        let actual = if let Some(handler) = supplied.get(&saved.key) {
101            let mut descriptor = handler.descriptor();
102            descriptor.key = saved.key.clone();
103            descriptor
104        } else {
105            index
106                .entity(&saved.key)
107                .filter(|entity| table.contains(*entity))
108                .and_then(|entity| world.get::<Bound>(entity))
109                .ok_or_else(|| CheckpointError::MissingHandler {
110                    key: saved.key.clone(),
111                })?
112                .descriptor
113                .clone()
114        };
115        if actual.family.family() != saved.family.family() {
116            return Err(CheckpointError::FamilyChanged {
117                key: saved.key.clone(),
118                saved: saved.family.family(),
119                found: actual.family.family(),
120            });
121        }
122        if mode == RestoreMode::Strict && actual != *saved {
123            return Err(CheckpointError::DescriptorChanged {
124                key: saved.key.clone(),
125            });
126        }
127        descriptors.insert(saved.key.clone(), actual);
128    }
129    for key in supplied.keys() {
130        if !descriptors.contains_key(key) {
131            return Err(CheckpointError::UnrequiredHandler { key: key.clone() });
132        }
133    }
134
135    let loaded = super::load_state(checkpoint, world, assets, aliases)?;
136    for entity in &loaded.entities {
137        let Some(bound) = world.get::<Bound>(*entity) else {
138            continue;
139        };
140        let key = bound.key.clone();
141        // Keep saved dispatch keys so restored effects resolve to these implementations.
142        if let Some(descriptor) = descriptors.remove(&key) {
143            world.entity_mut(*entity).insert(Bound {
144                key: key.clone(),
145                descriptor,
146            });
147        }
148        if let Some(handler) = supplied.remove(&key) {
149            HandlerTable::install(world, *entity, handler);
150        }
151    }
152    Ok(loaded)
153}