Skip to main content

wire/
object_availability.rs

1// SPDX-License-Identifier: Apache-2.0
2use objects::store::ObjectStore;
3
4use crate::{ObjectId, ObjectInfo, ObjectType, Result};
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub struct ObjectAvailabilityPlan {
8    pub have_objects: Vec<ObjectId>,
9    pub want_objects: Vec<ObjectId>,
10    pub present_objects: Vec<ObjectId>,
11    pub missing_objects: Vec<ObjectId>,
12    pub resumable_objects: Vec<ObjectId>,
13    pub lazy_objects: Vec<ObjectId>,
14    pub partial_fetch_allowed: bool,
15}
16
17pub fn has_object(store: &impl ObjectStore, info: &ObjectInfo) -> Result<bool> {
18    match (&info.id, info.obj_type) {
19        (ObjectId::Hash(hash), ObjectType::Blob) => Ok(store.has_blob(hash)?),
20        (ObjectId::Hash(hash), ObjectType::Tree) => Ok(store.has_tree(hash)?),
21        // State identity deliberately excludes mutable tail fields such as
22        // discussions and review signatures. A receiver may have the same
23        // ChangeId with older tail pointers, so refresh State records by id
24        // and let the store's loose-state shadowing keep the newest metadata
25        // body authoritative.
26        (ObjectId::ChangeId(_), ObjectType::State) => Ok(false),
27        // Redactions are keyed by the redacted blob's hash. Two senders
28        // can declare different redactions on the same blob (different
29        // reason / signature / timestamp), so we conservatively report
30        // "do not have" and always re-fetch — `accept_wire_redactions`
31        // deduplicates via the content-addressed `put_redaction`
32        // idempotency rule. Cheap to refetch; correct under merge.
33        (ObjectId::Hash(_), ObjectType::Redaction) => Ok(false),
34        // StateVisibility is a per-state sidecar with append/merge
35        // semantics. Like Redaction, conservatively refetch and let the
36        // repository boundary validate + dedupe.
37        (ObjectId::ChangeId(_), ObjectType::StateVisibility) => Ok(false),
38        _ => Ok(false),
39    }
40}
41
42pub fn plan_object_availability(
43    store: &impl ObjectStore,
44    objects: &[ObjectInfo],
45) -> Result<ObjectAvailabilityPlan> {
46    let mut plan = ObjectAvailabilityPlan::default();
47
48    for info in objects {
49        if has_object(store, info)? {
50            plan.have_objects.push(info.id.clone());
51            plan.present_objects.push(info.id.clone());
52        } else {
53            plan.want_objects.push(info.id.clone());
54            plan.missing_objects.push(info.id.clone());
55        }
56    }
57
58    Ok(plan)
59}
60
61impl ObjectAvailabilityPlan {
62    pub fn with_partial_fetch_allowed(mut self, allowed: bool) -> Self {
63        self.partial_fetch_allowed = allowed;
64        self
65    }
66
67    pub fn is_complete(&self) -> bool {
68        self.want_objects.is_empty()
69            && self.missing_objects.is_empty()
70            && self.resumable_objects.is_empty()
71            && self.lazy_objects.is_empty()
72    }
73
74    pub fn has_partial_fetch_candidates(&self) -> bool {
75        !self.resumable_objects.is_empty() || !self.lazy_objects.is_empty()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use objects::{
82        object::{Blob, ChangeId, ContentHash, Tree},
83        store::{ObjectStore, Result as StoreResult},
84    };
85
86    use super::*;
87
88    #[derive(Default)]
89    struct DummyStore {
90        blob: Option<ContentHash>,
91    }
92
93    impl ObjectStore for DummyStore {
94        fn get_blob(&self, _hash: &ContentHash) -> StoreResult<Option<Blob>> {
95            Ok(None)
96        }
97
98        fn put_blob(&self, _blob: &Blob) -> StoreResult<ContentHash> {
99            unreachable!("not used in test")
100        }
101
102        fn has_blob(&self, hash: &ContentHash) -> StoreResult<bool> {
103            Ok(self.blob == Some(*hash))
104        }
105
106        fn get_tree(&self, _hash: &ContentHash) -> StoreResult<Option<Tree>> {
107            Ok(None)
108        }
109
110        fn put_tree(&self, _tree: &Tree) -> StoreResult<ContentHash> {
111            unreachable!("not used in test")
112        }
113
114        fn has_tree(&self, _hash: &ContentHash) -> StoreResult<bool> {
115            Ok(false)
116        }
117
118        fn get_state(&self, _id: &ChangeId) -> StoreResult<Option<objects::object::State>> {
119            Ok(None)
120        }
121
122        fn put_state(&self, _state: &objects::object::State) -> StoreResult<()> {
123            unreachable!("not used in test")
124        }
125
126        fn has_state(&self, _id: &ChangeId) -> StoreResult<bool> {
127            Ok(false)
128        }
129
130        fn list_states(&self) -> StoreResult<Vec<ChangeId>> {
131            Ok(vec![])
132        }
133
134        fn get_action(
135            &self,
136            _id: &objects::object::ActionId,
137        ) -> StoreResult<Option<objects::object::Action>> {
138            Ok(None)
139        }
140
141        fn put_action(
142            &self,
143            _action: &mut objects::object::Action,
144        ) -> StoreResult<objects::object::ActionId> {
145            unreachable!("not used in test")
146        }
147
148        fn list_actions(&self) -> StoreResult<Vec<objects::object::ActionId>> {
149            Ok(vec![])
150        }
151
152        fn list_blobs(&self) -> StoreResult<Vec<ContentHash>> {
153            Ok(vec![])
154        }
155
156        fn list_trees(&self) -> StoreResult<Vec<ContentHash>> {
157            Ok(vec![])
158        }
159    }
160
161    #[test]
162    fn test_plan_tracks_present_and_missing_objects() {
163        let blob = Blob::new(b"hello".to_vec());
164        let blob_hash = blob.hash();
165        let store = DummyStore {
166            blob: Some(blob_hash),
167        };
168        let missing_hash = ContentHash::from_bytes([7; 32]);
169        let objects = vec![
170            ObjectInfo {
171                id: ObjectId::Hash(blob_hash),
172                obj_type: ObjectType::Blob,
173                size: blob.size() as u64,
174                delta_base: None,
175            },
176            ObjectInfo {
177                id: ObjectId::Hash(missing_hash),
178                obj_type: ObjectType::Tree,
179                size: 0,
180                delta_base: None,
181            },
182        ];
183
184        let plan = plan_object_availability(&store, &objects).unwrap();
185
186        assert_eq!(plan.have_objects.len(), 1);
187        assert_eq!(plan.want_objects.len(), 1);
188        assert_eq!(plan.present_objects.len(), 1);
189        assert_eq!(plan.missing_objects.len(), 1);
190        assert!(!plan.is_complete());
191    }
192
193    #[test]
194    fn state_objects_are_refreshed_even_when_present_locally() {
195        let store = DummyStore::default();
196        let state = ChangeId::from_bytes([9; 16]);
197        let objects = vec![ObjectInfo {
198            id: ObjectId::ChangeId(state),
199            obj_type: ObjectType::State,
200            size: 0,
201            delta_base: None,
202        }];
203
204        let plan = plan_object_availability(&store, &objects).unwrap();
205
206        assert!(plan.have_objects.is_empty());
207        assert_eq!(plan.want_objects, vec![ObjectId::ChangeId(state)]);
208        assert_eq!(plan.missing_objects, vec![ObjectId::ChangeId(state)]);
209    }
210
211    #[test]
212    fn test_partial_fetch_flag_helpers() {
213        let plan = ObjectAvailabilityPlan::default().with_partial_fetch_allowed(true);
214
215        assert!(plan.partial_fetch_allowed);
216        assert!(!plan.has_partial_fetch_candidates());
217        assert!(plan.is_complete());
218    }
219}