noosphere_core/data/
versioned_map.rs

1use anyhow::Result;
2use cid::Cid;
3use libipld_cbor::DagCborCodec;
4use serde::{de::DeserializeOwned, Deserialize, Serialize};
5use std::{fmt::Display, hash::Hash, marker::PhantomData};
6
7use noosphere_collections::hamt::{Hamt, Hash as HamtHash, Sha256};
8use noosphere_storage::BlockStore;
9
10use noosphere_common::ConditionalSync;
11
12use super::{ChangelogIpld, DelegationIpld, IdentityIpld, Jwt, Link, MemoIpld, RevocationIpld};
13
14/// A [VersionedMapIpld] that represents the content space of a sphere
15pub type ContentIpld = VersionedMapIpld<String, Link<MemoIpld>>;
16/// A [VersionedMapIpld] that represents the petname space of a sphere
17pub type IdentitiesIpld = VersionedMapIpld<String, IdentityIpld>;
18/// A [VersionedMapIpld] that represents the key authorizations in a sphere
19pub type DelegationsIpld = VersionedMapIpld<Link<Jwt>, DelegationIpld>;
20/// A [VersionedMapIpld] that represents the authority revocations in a sphere
21pub type RevocationsIpld = VersionedMapIpld<Link<Jwt>, RevocationIpld>;
22
23/// A helper trait to simplify expressing the bounds of a valid [VersionedMapIpld] key
24pub trait VersionedMapKey:
25    Serialize + DeserializeOwned + HamtHash + Clone + Eq + Ord + ConditionalSync + Display
26{
27}
28
29impl<T> VersionedMapKey for T where
30    T: Serialize + DeserializeOwned + HamtHash + Clone + Eq + Ord + ConditionalSync + Display
31{
32}
33
34/// A helper trait to simplify expressing the bounds of a valid [VersionedMapIpld] value
35pub trait VersionedMapValue:
36    Serialize + DeserializeOwned + Clone + Eq + Hash + ConditionalSync
37{
38}
39
40impl<T> VersionedMapValue for T where
41    T: Serialize + DeserializeOwned + Clone + Eq + Hash + ConditionalSync
42{
43}
44
45/// A [MapOperation] represents a single change to a [VersionedMapIpld] as it may
46/// be recorded in a [ChangelogIpld].
47#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
48pub enum MapOperation<Key, Value> {
49    /// A [MapOperation] that represents an update or insert to a [VersionedMapIpld]
50    Add {
51        /// The key that was updated or inserted
52        key: Key,
53        /// The new value associated with the key
54        value: Value,
55    },
56    /// A [MapOperation] that represents a removal of a key from a [VersionedMapIpld]
57    Remove {
58        /// The key that was removed
59        key: Key,
60    },
61}
62
63/// A [VersionedMapIpld] pairs a [Hamt] and a [ChangelogIpld] to enable a data structure
64/// that contains its difference from a historical ancestor without requiring that a diff
65/// be performed.
66#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize)]
67pub struct VersionedMapIpld<Key, Value>
68where
69    Key: VersionedMapKey,
70    Value: VersionedMapValue,
71{
72    /// A pointer to a [Hamt] root
73    pub hamt: Cid,
74    // TODO(#262): The size of this vec is implicitly limited by the IPLD block
75    // size limit. This is probably fine most of the time; the vec only holds
76    // the delta changes, and N<10 probably holds in the majority of cases. But,
77    // it will be necessary to gracefully survive the outlier cases where
78    // N>~1000.
79    /// A pointer to a [ChangelogIpld]
80    pub changelog: Cid,
81
82    #[allow(missing_docs)]
83    #[serde(skip)]
84    pub signature: PhantomData<(Key, Value)>,
85}
86
87impl<Key, Value> VersionedMapIpld<Key, Value>
88where
89    Key: VersionedMapKey,
90    Value: VersionedMapValue,
91{
92    /// Load the [Hamt] root associated with this [VersionedMapIpld]
93    pub async fn load_hamt<S: BlockStore>(&self, store: &S) -> Result<Hamt<S, Value, Key, Sha256>> {
94        Hamt::load(&self.hamt, store.clone()).await
95    }
96
97    /// Load the [ChangelogIpld] associated with this [VersionedMapIpld]
98    pub async fn load_changelog<S: BlockStore>(
99        &self,
100        store: &S,
101    ) -> Result<ChangelogIpld<MapOperation<Key, Value>>> {
102        store.load::<DagCborCodec, _>(&self.changelog).await
103    }
104
105    /// Initialize an empty [VersionedMapIpld], creating an empty [Hamt] root
106    /// and [ChangelogIpld] as well
107    // NOTE: We currently don't have a mechanism to prepuplate the store with
108    // "empty" DAGs like a HAMT. So, we do it lazily by requiring async
109    // initialization of this struct even when it is empty.
110    pub async fn empty<S: BlockStore>(store: &mut S) -> Result<Self> {
111        let mut hamt = Hamt::<S, Value, Key, Sha256>::new(store.clone());
112        let changelog = ChangelogIpld::<MapOperation<Key, Value>>::default();
113
114        let changelog_cid = store.save::<DagCborCodec, _>(&changelog).await?;
115        let hamt_cid = hamt.flush().await?;
116
117        Ok(VersionedMapIpld {
118            hamt: hamt_cid,
119            changelog: changelog_cid,
120            signature: Default::default(),
121        })
122    }
123}