spacetimedb_table/
blob_store.rs1use blake3::hash;
15use spacetimedb_data_structures::map::{Entry, HashMap};
16use spacetimedb_lib::{de::Deserialize, ser::Serialize};
17use spacetimedb_memory_usage::MemoryUsage;
18
19#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Hash, Debug, Serialize, Deserialize)]
21pub struct BlobHash {
22 pub data: [u8; BlobHash::SIZE],
26}
27
28impl MemoryUsage for BlobHash {}
29
30impl BlobHash {
31 pub const SIZE: usize = 32;
33
34 pub fn hash_from_bytes(bytes: &[u8]) -> Self {
36 let data = hash(bytes).into();
37 Self { data }
38 }
39}
40
41impl TryFrom<&[u8]> for BlobHash {
42 type Error = ();
43
44 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
45 let data: [u8; Self::SIZE] = data.try_into().map_err(drop)?;
46 Ok(Self { data })
47 }
48}
49
50#[derive(Debug)]
52pub struct NoSuchBlobError;
53
54pub type BlobsIter<'a> = Box<dyn Iterator<Item = (&'a BlobHash, usize, &'a [u8])> + 'a>;
61
62pub trait BlobStore: Sync {
67 fn clone_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError>;
75
76 fn insert_blob(&mut self, bytes: &[u8]) -> BlobHash;
81
82 fn insert_with_uses(&mut self, hash: &BlobHash, uses: usize, bytes: Box<[u8]>);
86
87 fn retrieve_blob(&self, hash: &BlobHash) -> Result<&[u8], NoSuchBlobError>;
89
90 fn free_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError>;
96
97 fn iter_blobs(&self) -> BlobsIter<'_>;
106
107 fn bytes_used_by_blobs(&self) -> u64 {
114 self.iter_blobs()
115 .map(|(_, uses, data)| data.len() as u64 * uses as u64)
116 .sum()
117 }
118
119 fn num_blobs(&self) -> u64 {
125 self.iter_blobs().map(|(_, uses, _)| uses as u64).sum()
126 }
127}
128
129#[derive(Default)]
132pub struct NullBlobStore;
133
134impl BlobStore for NullBlobStore {
135 fn clone_blob(&mut self, _hash: &BlobHash) -> Result<(), NoSuchBlobError> {
136 unimplemented!("NullBlobStore doesn't do anything")
137 }
138
139 fn insert_blob(&mut self, _bytes: &[u8]) -> BlobHash {
140 unimplemented!("NullBlobStore doesn't do anything")
141 }
142
143 fn insert_with_uses(&mut self, _hash: &BlobHash, _uses: usize, _bytes: Box<[u8]>) {
144 unimplemented!("NullBlobStore doesn't do anything")
145 }
146
147 fn retrieve_blob(&self, _hash: &BlobHash) -> Result<&[u8], NoSuchBlobError> {
148 unimplemented!("NullBlobStore doesn't do anything")
149 }
150
151 fn free_blob(&mut self, _hash: &BlobHash) -> Result<(), NoSuchBlobError> {
152 unimplemented!("NullBlobStore doesn't do anything")
153 }
154
155 fn iter_blobs(&self) -> BlobsIter<'_> {
156 unimplemented!("NullBlobStore doesn't do anything")
157 }
158}
159
160#[derive(Default, PartialEq, Eq, Debug)]
163pub struct HashMapBlobStore {
164 map: HashMap<BlobHash, BlobObject>,
167}
168
169impl MemoryUsage for HashMapBlobStore {
170 fn heap_usage(&self) -> usize {
171 let Self { map } = self;
172 map.heap_usage()
173 }
174}
175
176#[derive(PartialEq, Eq, Debug)]
178struct BlobObject {
179 uses: usize,
181 blob: Box<[u8]>,
183}
184
185impl MemoryUsage for BlobObject {
186 fn heap_usage(&self) -> usize {
187 let Self { uses, blob } = self;
188 uses.heap_usage() + blob.heap_usage()
189 }
190}
191
192impl BlobStore for HashMapBlobStore {
193 fn clone_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError> {
194 self.map.get_mut(hash).ok_or(NoSuchBlobError)?.uses += 1;
195 Ok(())
196 }
197
198 fn insert_blob(&mut self, bytes: &[u8]) -> BlobHash {
199 let hash = BlobHash::hash_from_bytes(bytes);
200 self.map
201 .entry(hash)
202 .and_modify(|v| v.uses += 1)
203 .or_insert_with(|| BlobObject {
204 blob: bytes.into(),
205 uses: 1,
206 });
207 hash
208 }
209
210 fn insert_with_uses(&mut self, hash: &BlobHash, uses: usize, bytes: Box<[u8]>) {
211 debug_assert_eq!(hash, &BlobHash::hash_from_bytes(&bytes));
212 self.map
213 .entry(*hash)
214 .and_modify(|v| v.uses += uses)
215 .or_insert_with(|| BlobObject { blob: bytes, uses });
216 }
217
218 fn retrieve_blob(&self, hash: &BlobHash) -> Result<&[u8], NoSuchBlobError> {
219 self.map.get(hash).map(|obj| &*obj.blob).ok_or(NoSuchBlobError)
220 }
221
222 fn free_blob(&mut self, hash: &BlobHash) -> Result<(), NoSuchBlobError> {
223 match self.map.entry(*hash) {
224 Entry::Vacant(_) => return Err(NoSuchBlobError),
225 Entry::Occupied(entry) if entry.get().uses == 1 => drop(entry.remove()),
226 Entry::Occupied(mut entry) => entry.get_mut().uses -= 1,
227 }
228 Ok(())
229 }
230
231 fn iter_blobs(&self) -> BlobsIter<'_> {
232 Box::new(self.map.iter().map(|(hash, obj)| (hash, obj.uses, &obj.blob[..])))
233 }
234}
235
236#[cfg(test)]
237impl HashMapBlobStore {
238 fn iter(&self) -> impl Iterator<Item = (&BlobHash, usize, &[u8])> + '_ {
240 self.map.iter().map(|(hash, obj)| (hash, obj.uses, &*obj.blob))
241 }
242
243 pub fn usage_counter(&self) -> HashMap<BlobHash, usize> {
245 self.iter().map(|(hash, uses, _)| (*hash, uses)).collect()
246 }
247}