Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle: identity caching
Archived fintohaps opened 2 years ago

Add caching for the Identity COB.

A migration file is added for the sqlite COB cache, which inserts the new identities table.

A new module identity::cache is added, similar to patch::cache and issue::cache.

To achieve the same pattern as patches and issues, it was necessary to introduce a new type Identities in the identity module, which offers the same functionality as methods that existed on the Identity type.

The caching is implemented using write-through semantics, again similar to patches and issues.

19 files changed +338 -130 6569449f 18b0f389
modified radicle-cli/src/commands/id.rs
@@ -272,7 +272,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    let repo = storage
        .repository(rid)
        .context(anyhow!("repository `{rid}` not found in local storage"))?;
-
    let mut identity = Identity::load_mut(&repo)?;
+
    let mut identities = profile.identities_mut(&repo)?;
+
    let mut identity = identities.load_mut()?;
    let current = identity.current().clone();

    match options.op {
@@ -613,13 +614,18 @@ and description.
    Ok(result)
}

-
fn update<R: WriteRepository + cob::Store, G: Signer>(
+
fn update<R, C, G>(
    title: Option<String>,
    description: Option<String>,
    doc: Doc<Verified>,
-
    current: &mut IdentityMut<R>,
+
    current: &mut IdentityMut<R, C>,
    signer: &G,
-
) -> anyhow::Result<Revision> {
+
) -> anyhow::Result<Revision>
+
where
+
    R: WriteRepository + cob::Store,
+
    C: cob::cache::Update<Identity>,
+
    G: Signer,
+
{
    if let Some((title, description)) = edit_title_description(title, description)? {
        let id = current.update(title, description, &doc, signer)?;
        let revision = current
modified radicle-cli/src/commands/inbox.rs
@@ -6,8 +6,8 @@ use anyhow::anyhow;

use git_ref_format::Qualified;
use localtime::LocalTime;
+
use radicle::cob::identity::cache::Identities as _;
use radicle::cob::TypedId;
-
use radicle::identity::Identity;
use radicle::issue::cache::Issues as _;
use radicle::node::notifications;
use radicle::node::notifications::*;
@@ -273,6 +273,7 @@ where
    let proj = doc.project()?;
    let issues = profile.issues(&repo)?;
    let patches = profile.patches(&repo)?;
+
    let identities = profile.identities(&repo)?;

    let mut notifs = notifs.by_repo(&rid, sort_by.field)?.collect::<Vec<_>>();
    if !sort_by.reverse {
@@ -306,7 +307,7 @@ where
        } = match &n.kind {
            NotificationKind::Branch { name } => NotificationRow::branch(name, head, &n, &repo)?,
            NotificationKind::Cob { typed_id } => {
-
                match NotificationRow::cob(typed_id, &n, &issues, &patches, &repo)? {
+
                match NotificationRow::cob(typed_id, &n, &issues, &patches, &identities)? {
                    Some(row) => row,
                    None => continue,
                }
@@ -401,17 +402,17 @@ impl NotificationRow {
        ))
    }

-
    fn cob<S, I, P>(
+
    fn cob<I, P, Ids>(
        typed_id: &TypedId,
        n: &Notification,
        issues: &I,
        patches: &P,
-
        repo: &S,
+
        identities: &Ids,
    ) -> anyhow::Result<Option<Self>>
    where
-
        S: ReadRepository + cob::Store,
        I: cob::issue::cache::Issues,
        P: cob::patch::cache::Patches,
+
        Ids: cob::identity::cache::Identities,
    {
        let TypedId { id, .. } = typed_id;
        let (category, summary, state) = if typed_id.is_issue() {
@@ -435,7 +436,7 @@ impl NotificationRow {
                term::format::patch::state(patch.state()),
            )
        } else if typed_id.is_identity() {
-
            let Ok(identity) = Identity::get(id, repo) else {
+
            let Ok(identity) = identities.get(id) else {
                log::error!(
                    target: "cli",
                    "Error retrieving identity {id} for notification {}", n.id
@@ -548,7 +549,8 @@ fn show(
            term::patch::show(&patch, &typed_id.id, false, &repo, None, profile)?;
        }
        NotificationKind::Cob { typed_id } if typed_id.is_identity() => {
-
            let identity = Identity::get(&typed_id.id, &repo)?;
+
            let identities = profile.identities(&repo)?;
+
            let identity = identities.get(&typed_id.id)?;

            term::json::to_pretty(&identity.doc, Path::new("radicle.json"))?.print();
        }
modified radicle-cli/src/commands/init.rs
@@ -17,7 +17,7 @@ use radicle::identity::{RepoId, Visibility};
use radicle::node::policy::Scope;
use radicle::node::{Event, Handle, NodeId};
use radicle::prelude::Doc;
-
use radicle::{profile, Node};
+
use radicle::{cob, profile, Node};

use crate as cli;
use crate::commands;
@@ -265,8 +265,13 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
    let mut spinner = term::spinner("Initializing...");
    let mut push_cmd = String::from("git push");

+
    // N.b. we cannot use `profile.identities_mut` here because the
+
    // Repository has not been initialised.
+
    let mut cache = cob::cache::Store::open(profile.cobs().join(cob::cache::COBS_DB_FILE))?;
+

    match radicle::rad::init(
        &repo,
+
        &mut cache,
        &name,
        &description,
        branch.clone(),
modified radicle-cli/src/commands/inspect.rs
@@ -7,8 +7,9 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _};
use chrono::prelude::*;

+
use radicle::cob::identity::cache::Identities as _;
+
use radicle::identity::DocAt;
use radicle::identity::RepoId;
-
use radicle::identity::{DocAt, Identity};
use radicle::node::policy::Policy;
use radicle::node::AliasStore as _;
use radicle::storage::git::{Repository, Storage};
@@ -213,7 +214,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
        }
        Target::History => {
            let (repo, _) = repo(rid, storage)?;
-
            let identity = Identity::load(&repo)?;
+
            let identities = profile.identities(&repo)?;
+
            let identity = identities.load()?;
            let head = repo.identity_head()?;
            let history = repo.revwalk(head)?;

modified radicle-cli/src/commands/publish.rs
@@ -2,7 +2,7 @@ use std::ffi::OsString;

use anyhow::{anyhow, Context as _};

-
use radicle::identity::{Identity, Visibility};
+
use radicle::identity::Visibility;
use radicle::node::Handle as _;
use radicle::prelude::RepoId;
use radicle::storage::{SignRepository, ValidateRepository, WriteRepository, WriteStorage};
@@ -76,7 +76,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    };

    let repo = profile.storage.repository_mut(rid)?;
-
    let mut identity = Identity::load_mut(&repo)?;
+
    let mut identities = profile.identities_mut(&repo)?;
+
    let mut identity = identities.load_mut()?;
    let mut doc = identity.doc().clone();

    if doc.visibility.is_public() {
modified radicle-httpd/src/test.rs
@@ -19,7 +19,7 @@ use radicle::git::{raw as git2, RefString};
use radicle::identity::Visibility;
use radicle::profile::Home;
use radicle::storage::ReadStorage;
-
use radicle::Storage;
+
use radicle::{cob, Storage};
use radicle::{node, profile};
use radicle_crypto::test::signer::MockSigner;

@@ -129,8 +129,10 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
    let visibility = Visibility::Private {
        allow: BTreeSet::default(),
    };
+
    let mut cache = cob::cache::NoCache;
    let (rid, _, _) = radicle::rad::init(
        &repo,
+
        &mut cache,
        &name,
        &description,
        branch,
@@ -221,8 +223,10 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
    let description = "Rad repository for tests".to_string();
    let branch = RefString::try_from(DEFAULT_BRANCH).unwrap();
    let visibility = Visibility::default();
+
    let mut cache = cob::cache::NoCache;
    let (rid, _, _) = radicle::rad::init(
        &repo,
+
        &mut cache,
        &name,
        &description,
        branch,
modified radicle-node/src/test/environment.rs
@@ -153,6 +153,7 @@ pub struct Node<G> {
    pub config: Config,
    pub db: service::Stores<Database>,
    pub policies: policy::Store<policy::Write>,
+
    pub cache: cob::cache::StoreWriter,
}

impl Node<MemorySigner> {
@@ -163,6 +164,8 @@ impl Node<MemorySigner> {
        let policies = policy::Store::open(policies_db).unwrap();
        let db = profile.database_mut().unwrap();
        let db = service::Stores::from(db);
+
        let cache = profile.home.cobs().join(COBS_DB_FILE);
+
        let cache = cob::cache::Store::open(cache).unwrap();

        Node {
            id,
@@ -172,6 +175,7 @@ impl Node<MemorySigner> {
            db,
            policies,
            storage: profile.storage,
+
            cache,
        }
    }
}
@@ -412,6 +416,8 @@ impl Node<MockSigner> {
        let policies = home.policies_mut().unwrap();
        let db = home.database_mut().unwrap();
        let db = service::Stores::from(db);
+
        let cache = home.cobs().join(COBS_DB_FILE);
+
        let cache = cob::cache::Store::open(cache).unwrap();

        log::debug!(target: "test", "Node::init {}: {}", config.alias, signer.public_key());
        Self {
@@ -422,6 +428,7 @@ impl Node<MockSigner> {
            config,
            db,
            policies,
+
            cache,
        }
    }
}
@@ -469,6 +476,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
        let branch = refname!("master");
        let id = rad::init(
            repo,
+
            &mut self.cache,
            name,
            description,
            branch.clone(),
modified radicle-node/src/test/peer.rs
@@ -6,6 +6,7 @@ use std::str::FromStr;

use log::*;

+
use radicle::cob;
use radicle::identity::Visibility;
use radicle::node::address::Store as _;
use radicle::node::Database;
@@ -136,8 +137,10 @@ impl<G: Signer> Peer<Storage, G> {
        radicle::storage::git::transport::local::register(self.storage().clone());

        let (repo, _) = fixtures::repository(self.tempdir.path().join(name));
+
        let mut cache = cob::cache::NoCache;
        let (rid, _, _) = rad::init(
            &repo,
+
            &mut cache,
            name,
            description,
            radicle::git::refname!("master"),
modified radicle-node/src/tests.rs
@@ -8,6 +8,7 @@ use std::time;

use crossbeam_channel as chan;
use netservices::Direction as Link;
+
use radicle::cob;
use radicle::identity::Visibility;
use radicle::node::address::Store;
use radicle::node::routing::Store as _;
@@ -1570,10 +1571,11 @@ fn test_push_and_pull() {
        eve.address(),
        ConnectOptions::default(),
    ));
-

+
    let mut cache = cob::cache::NoCache;
    // Alice creates a new project.
    let (proj_id, _, _) = rad::init(
        &repo,
+
        &mut cache,
        "alice",
        "alice's repo",
        git::refname!("master"),
modified radicle-node/src/worker/fetch.rs
@@ -210,11 +210,14 @@ fn cache_cobs<S, C>(
) -> Result<(), error::Cache>
where
    S: ReadRepository + cob::Store,
-
    C: cob::cache::Update<cob::issue::Issue> + cob::cache::Update<cob::patch::Patch>,
+
    C: cob::cache::Update<cob::issue::Issue>
+
        + cob::cache::Update<cob::patch::Patch>
+
        + cob::cache::Update<cob::identity::Identity>,
    C: cob::cache::Remove<cob::issue::Issue> + cob::cache::Remove<cob::patch::Patch>,
{
    let issues = cob::issue::Issues::open(storage)?;
    let patches = cob::patch::Patches::open(storage)?;
+
    let identities = cob::identity::Identities::open(storage)?;
    for update in refs {
        match update {
            RefUpdate::Updated { name, .. }
@@ -246,6 +249,17 @@ where
                                    err: e.into(),
                                })?;
                        }
+
                    } else if identifier.is_identity() {
+
                        if let Ok(identity) = identities.get(&identifier.id) {
+
                            cache
+
                                .update(rid, &identifier.id, &identity)
+
                                .map(|_| ())
+
                                .map_err(|e| error::Cache::Update {
+
                                    id: identifier.id,
+
                                    type_name: identifier.type_name,
+
                                    err: e.into(),
+
                                })?;
+
                        }
                    }
                }
                None => continue,
modified radicle-tools/src/rad-init.rs
@@ -1,6 +1,6 @@
use std::path::Path;

-
use radicle::{git, identity::Visibility, Profile};
+
use radicle::{cob, git, identity::Visibility, Profile};

fn main() -> anyhow::Result<()> {
    let cwd = Path::new(".").canonicalize()?;
@@ -8,8 +8,10 @@ fn main() -> anyhow::Result<()> {
    let repo = radicle::git::raw::Repository::open(cwd)?;
    let profile = Profile::load()?;
    let signer = profile.signer()?;
+
    let mut cache = cob::cache::Store::open(profile.cobs().join(cob::cache::COBS_DB_FILE))?;
    let (id, _, _) = radicle::rad::init(
        &repo,
+
        &mut cache,
        &name,
        "",
        git::refname!("master"),
modified radicle/src/cob/cache.rs
@@ -23,7 +23,10 @@ const DB_WRITE_TIMEOUT: time::Duration = time::Duration::from_secs(6);

/// Database migrations.
/// The first migration is the creation of the initial tables.
-
const MIGRATIONS: &[&str] = &[include_str!("cache/migrations/1.sql")];
+
const MIGRATIONS: &[&str] = &[
+
    include_str!("cache/migrations/1.sql"),
+
    include_str!("cache/migrations/2.sql"),
+
];

#[derive(Error, Debug)]
pub enum Error {
@@ -247,6 +250,7 @@ where
///
/// The intention is for this to be used in tests that do not expect
/// any cache reads.
+
#[derive(Clone, Copy)]
pub struct NoCache;

impl<T> Update<T> for NoCache {
modified radicle/src/cob/identity.rs
@@ -1,3 +1,5 @@
+
pub mod cache;
+

use std::collections::BTreeMap;
use std::{fmt, ops::Deref, str::FromStr};

@@ -21,11 +23,13 @@ use crate::{
        doc::{Doc, DocError, RepoId},
        Did,
    },
-
    storage::{ReadRepository, RepositoryError, WriteRepository},
+
    storage::{HasRepoId, ReadRepository, RepositoryError, WriteRepository},
};

use super::{Author, EntryId};

+
pub use cache::Cache;
+

/// Type name of an identity proposal.
pub static TYPENAME: Lazy<TypeName> =
    Lazy::new(|| FromStr::from_str("xyz.radicle.id").expect("type name is valid"));
@@ -130,10 +134,23 @@ pub enum Error {
    Doc(#[from] DocError),
    #[error("revision {0} was not found")]
    NotFound(RevisionId),
+
    #[error("failed to update identity {id} in cache: {err}")]
+
    CacheUpdate {
+
        id: ObjectId,
+
        #[source]
+
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
+
    },
+
    #[error("failed to remove identity {id} from cache : {err}")]
+
    CacheRemove {
+
        id: ObjectId,
+
        #[source]
+
        err: Box<dyn std::error::Error + Send + Sync + 'static>,
+
    },
}

/// An evolving identity document.
-
#[derive(Debug, Clone, PartialEq, Eq)]
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
pub struct Identity {
    /// The canonical identifier for this identity.
    /// This is the object id of the initial document blob.
@@ -179,64 +196,6 @@ impl Identity {
            timeline: vec![root],
        }
    }
-

-
    pub fn initialize<'a, R: WriteRepository + cob::Store, G: Signer>(
-
        doc: &Doc<Verified>,
-
        store: &'a R,
-
        signer: &G,
-
    ) -> Result<IdentityMut<'a, R>, cob::store::Error> {
-
        let mut store = cob::store::Store::open(store)?;
-
        let (id, identity) =
-
            Transaction::<Identity, _>::initial("Initialize identity", &mut store, signer, |tx| {
-
                tx.revision("Initial revision", "", doc, None, signer)
-
            })?;
-

-
        Ok(IdentityMut {
-
            id,
-
            identity,
-
            store,
-
        })
-
    }
-

-
    pub fn get<R: ReadRepository + cob::Store>(
-
        object: &ObjectId,
-
        repo: &R,
-
    ) -> Result<Identity, store::Error> {
-
        cob::get::<Self, _>(repo, Self::type_name(), object)
-
            .map(|r| r.map(|cob| cob.object))?
-
            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
-
    }
-

-
    /// Get a proposal mutably.
-
    pub fn get_mut<'a, R: WriteRepository + cob::Store>(
-
        id: &ObjectId,
-
        repo: &'a R,
-
    ) -> Result<IdentityMut<'a, R>, store::Error> {
-
        let obj = Self::get(id, repo)?;
-
        let store = cob::store::Store::open(repo)?;
-

-
        Ok(IdentityMut {
-
            id: *id,
-
            identity: obj,
-
            store,
-
        })
-
    }
-

-
    pub fn load<R: ReadRepository + cob::Store>(repo: &R) -> Result<Identity, RepositoryError> {
-
        let oid = repo.identity_root()?;
-
        let oid = ObjectId::from(oid);
-

-
        Self::get(&oid, repo).map_err(RepositoryError::from)
-
    }
-

-
    pub fn load_mut<R: WriteRepository + cob::Store>(
-
        repo: &R,
-
    ) -> Result<IdentityMut<R>, RepositoryError> {
-
        let oid = repo.identity_root()?;
-
        let oid = ObjectId::from(oid);
-

-
        Self::get_mut(&oid, repo).map_err(RepositoryError::from)
-
    }
}

impl Identity {
@@ -612,11 +571,12 @@ impl<R: ReadRepository> cob::Evaluate<R> for Identity {
    }
}

-
#[derive(Clone, Debug, PartialEq, Eq)]
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(tag = "verdict")]
pub enum Verdict {
    /// An accepting verdict must supply the [`Signature`] over the
    /// new proposed [`Doc`].
-
    Accept(Signature),
+
    Accept { signature: Signature },
    /// Rejecting the proposed [`Doc`].
    Reject,
}
@@ -656,7 +616,8 @@ impl std::fmt::Display for State {
///
/// Once a revision has reached the quorum threshold of the previous
/// [`Identity`] it is then adopted as the current identity.
-
#[derive(Clone, Debug, PartialEq, Eq)]
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
pub struct Revision {
    /// The id of this revision. Points to a commit.
    pub id: RevisionId,
@@ -692,7 +653,7 @@ impl std::ops::Deref for Revision {
impl Revision {
    pub fn signatures(&self) -> impl Iterator<Item = (&PublicKey, Signature)> {
        self.verdicts().filter_map(|(key, verdict)| match verdict {
-
            Verdict::Accept(sig) => Some((key, *sig)),
+
            Verdict::Accept { signature } => Some((key, *signature)),
            Verdict::Reject => None,
        })
    }
@@ -715,7 +676,7 @@ impl Revision {

    pub fn rejected(&self) -> impl Iterator<Item = Did> + '_ {
        self.verdicts().filter_map(|(key, v)| match v {
-
            Verdict::Accept(_) => None,
+
            Verdict::Accept { .. } => None,
            Verdict::Reject => Some(key.into()),
        })
    }
@@ -739,7 +700,7 @@ impl Revision {
        parent: Option<RevisionId>,
        timestamp: Timestamp,
    ) -> Self {
-
        let verdicts = BTreeMap::from_iter([(*author.public_key(), Verdict::Accept(signature))]);
+
        let verdicts = BTreeMap::from_iter([(*author.public_key(), Verdict::Accept { signature })]);

        Self {
            id,
@@ -770,7 +731,7 @@ impl Revision {
        }
        if self
            .verdicts
-
            .insert(author, Verdict::Accept(signature))
+
            .insert(author, Verdict::Accept { signature })
            .is_some()
        {
            return Err(ApplyError::DuplicateVerdict);
@@ -852,14 +813,15 @@ impl<R: ReadRepository> store::Transaction<Identity, R> {
    }
}

-
pub struct IdentityMut<'a, R> {
+
pub struct IdentityMut<'a, 'g, R, C> {
    pub id: ObjectId,

    identity: Identity,
-
    store: store::Store<'a, Identity, R>,
+
    store: &'g mut Identities<'a, R>,
+
    cache: &'g mut C,
}

-
impl<'a, R> fmt::Debug for IdentityMut<'a, R> {
+
impl<'a, 'g, R, C> fmt::Debug for IdentityMut<'a, 'g, R, C> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IdentityMut")
            .field("id", &self.id)
@@ -868,16 +830,14 @@ impl<'a, R> fmt::Debug for IdentityMut<'a, R> {
    }
}

-
impl<'a, R> IdentityMut<'a, R>
+
impl<'a, 'g, R, C> IdentityMut<'a, 'g, R, C>
where
    R: WriteRepository + cob::Store,
+
    C: cob::cache::Update<Identity>,
{
    /// Reload the identity data from storage.
    pub fn reload(&mut self) -> Result<(), store::Error> {
-
        self.identity = self
-
            .store
-
            .get(&self.id)?
-
            .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
+
        self.identity = self.store.get(&self.id)?;

        Ok(())
    }
@@ -895,7 +855,13 @@ where
        let mut tx = Transaction::default();
        operations(&mut tx)?;

-
        let (doc, commit) = tx.commit(message, self.id, &mut self.store, signer)?;
+
        let (doc, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
+
        self.cache
+
            .update(&self.store.rid(), &self.id, &doc)
+
            .map_err(|e| Error::CacheUpdate {
+
                id: self.id,
+
                err: e.into(),
+
            })?;
        self.identity = doc;

        Ok(commit)
@@ -963,7 +929,7 @@ where
    }
}

-
impl<'a, R> Deref for IdentityMut<'a, R> {
+
impl<'a, 'g, R, C> Deref for IdentityMut<'a, 'g, R, C> {
    type Target = Identity;

    fn deref(&self) -> &Self::Target {
@@ -971,6 +937,99 @@ impl<'a, R> Deref for IdentityMut<'a, R> {
    }
}

+
pub struct Identities<'a, R> {
+
    raw: store::Store<'a, Identity, R>,
+
}
+

+
impl<'a, R> HasRepoId for Identities<'a, R>
+
where
+
    R: ReadRepository,
+
{
+
    fn rid(&self) -> RepoId {
+
        self.raw.as_ref().id()
+
    }
+
}
+

+
impl<'a, R> Identities<'a, R>
+
where
+
    R: ReadRepository + cob::Store,
+
{
+
    /// Open an issues store.
+
    pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
+
        let raw = store::Store::open(repository)?;
+

+
        Ok(Self { raw })
+
    }
+

+
    pub fn initialize<'g, G, C>(
+
        &'g mut self,
+
        doc: &Doc<Verified>,
+
        cache: &'g mut C,
+
        signer: &G,
+
    ) -> Result<IdentityMut<'a, 'g, R, C>, Error>
+
    where
+
        R: WriteRepository,
+
        C: cob::cache::Update<Identity>,
+
        G: Signer,
+
    {
+
        let (id, identity) = Transaction::<Identity, _>::initial(
+
            "Initialize identity",
+
            &mut self.raw,
+
            signer,
+
            |tx| tx.revision("Initial revision", "", doc, None, signer),
+
        )?;
+
        cache
+
            .update(&self.rid(), &id, &identity)
+
            .map_err(|e| Error::CacheUpdate { id, err: e.into() })?;
+

+
        Ok(IdentityMut {
+
            id,
+
            identity,
+
            store: self,
+
            cache,
+
        })
+
    }
+

+
    pub fn get(&self, object: &ObjectId) -> Result<Identity, store::Error> {
+
        self.raw
+
            .get(object)?
+
            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
+
    }
+

+
    /// Get a proposal mutably.
+
    pub fn get_mut<'g, C>(
+
        &'g mut self,
+
        id: &ObjectId,
+
        cache: &'g mut C,
+
    ) -> Result<IdentityMut<'a, 'g, R, C>, store::Error> {
+
        let obj = self.get(id)?;
+

+
        Ok(IdentityMut {
+
            id: *id,
+
            identity: obj,
+
            store: self,
+
            cache,
+
        })
+
    }
+

+
    pub fn load(&self) -> Result<Identity, RepositoryError> {
+
        let oid = self.raw.as_ref().identity_root()?;
+
        let oid = ObjectId::from(oid);
+

+
        self.get(&oid).map_err(RepositoryError::from)
+
    }
+

+
    pub fn load_mut<'g, C>(
+
        &'g mut self,
+
        cache: &'g mut C,
+
    ) -> Result<IdentityMut<'a, 'g, R, C>, RepositoryError> {
+
        let oid = self.raw.as_ref().identity_root()?;
+
        let oid = ObjectId::from(oid);
+

+
        self.get_mut(&oid, cache).map_err(RepositoryError::from)
+
    }
+
}
+

mod lookup {
    use super::*;

@@ -1037,7 +1096,9 @@ mod test {
        let NodeWithRepo { node, repo } = NodeWithRepo::default();
        let bob = MockSigner::default();
        let signer = &node.signer;
-
        let mut identity = Identity::load_mut(&*repo).unwrap();
+
        let mut identities = Identities::open(&*repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+
        let mut identity = identities.load_mut(&mut cache).unwrap();
        let mut doc = identity.doc().clone();
        let title = "Identity update";
        let description = "";
@@ -1093,7 +1154,9 @@ mod test {
        let bob = MockSigner::default();
        let eve = MockSigner::default();
        let signer = &node.signer;
-
        let mut identity = Identity::load_mut(&*repo).unwrap();
+
        let mut identities = Identities::open(&*repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+
        let mut identity = identities.load_mut(&mut cache).unwrap();
        let mut doc = identity.doc().clone();
        let title = "Identity update";
        let description = "";
@@ -1143,7 +1206,12 @@ mod test {
        let alice = &network.alice;
        let bob = &network.bob;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identities = Identities::open(&*alice.repo).unwrap();
+
        let mut bob_identities = Identities::open(&*bob.repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+

+
        let mut alice_cache = cache;
+
        let mut alice_identity = alice_identities.load_mut(&mut alice_cache).unwrap();
        let mut alice_doc = alice_identity.doc().clone();

        alice_doc.delegate(bob.signer.public_key());
@@ -1153,7 +1221,7 @@ mod test {

        bob.repo.fetch(alice);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let mut bob_identity = bob_identities.load_mut(&mut cache).unwrap();
        let bob_doc = bob_identity.doc().clone();
        assert!(bob_doc.is_delegate(bob.signer.public_key()));

@@ -1195,7 +1263,11 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identities = Identities::open(&*alice.repo).unwrap();
+
        let mut bob_identities = Identities::open(&*bob.repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+

+
        let mut alice_identity = alice_identities.load_mut(&mut cache).unwrap();
        let mut alice_doc = alice_identity.doc().clone();

        alice_doc.delegate(bob.signer.public_key());
@@ -1214,7 +1286,7 @@ mod test {
        assert!(alice_identity.revision(&a1).is_some());
        assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let mut bob_identity = bob_identities.load_mut(&mut cache).unwrap();
        let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();

        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
@@ -1234,7 +1306,12 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identities = Identities::open(&*alice.repo).unwrap();
+
        let mut bob_identities = Identities::open(&*bob.repo).unwrap();
+
        let mut eve_identities = Identities::open(&*eve.repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+

+
        let mut alice_identity = alice_identities.load_mut(&mut cache).unwrap();
        let mut alice_doc = alice_identity.doc().clone();

        alice_doc.delegate(bob.signer.public_key());
@@ -1253,11 +1330,11 @@ mod test {
        bob.repo.fetch(alice);
        eve.repo.fetch(bob);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let mut bob_identity = bob_identities.load_mut(&mut cache).unwrap();
        let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();
        assert_eq!(bob_identity.current, a2);

-
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
+
        let mut eve_identity = eve_identities.load_mut(&mut cache).unwrap();
        let mut eve_doc = eve_identity.doc().clone();
        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
        let e1 = eve_identity
@@ -1291,7 +1368,12 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identities = Identities::open(&*alice.repo).unwrap();
+
        let mut bob_identities = Identities::open(&*bob.repo).unwrap();
+
        let mut eve_identities = Identities::open(&*eve.repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+

+
        let mut alice_identity = alice_identities.load_mut(&mut cache).unwrap();
        let mut alice_doc = alice_identity.doc().clone();

        alice_doc.delegate(bob.signer.public_key());
@@ -1311,11 +1393,11 @@ mod test {
        eve.repo.fetch(bob);

        // Bob accepts alice's revision.
-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let mut bob_identity = bob_identities.load_mut(&mut cache).unwrap();
        let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();

        // Eve rejects the revision, not knowing.
-
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
+
        let mut eve_identity = eve_identities.load_mut(&mut cache).unwrap();
        let e1 = eve_identity.reject(a2, &eve.signer).unwrap();
        assert!(eve_identity.revision(&a2).unwrap().is_active());

@@ -1360,7 +1442,13 @@ mod test {
        let bob = &network.bob;
        let eve = &network.eve;

-
        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
+
        let mut alice_identities = Identities::open(&*alice.repo).unwrap();
+
        let mut bob_identities = Identities::open(&*bob.repo).unwrap();
+
        let mut eve_identities = Identities::open(&*eve.repo).unwrap();
+
        let mut cache = cob::cache::NoCache;
+

+
        let mut alice_cache = cache;
+
        let mut alice_identity = alice_identities.load_mut(&mut alice_cache).unwrap();
        let mut alice_doc = alice_identity.doc().clone();

        alice.repo.fetch(bob);
@@ -1375,7 +1463,7 @@ mod test {
        bob.repo.fetch(alice);
        eve.repo.fetch(alice);

-
        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
+
        let mut bob_identity = bob_identities.load_mut(&mut cache).unwrap();
        let mut bob_doc = bob_identity.doc().clone();
        assert!(bob_doc.is_delegate(bob.signer.public_key()));

@@ -1396,7 +1484,7 @@ mod test {
        eve.repo.fetch(bob);

        // In the meantime, Eve does the same thing on her side.
-
        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
+
        let mut eve_identity = eve_identities.load_mut(&mut cache).unwrap();
        let mut eve_doc = eve_identity.doc().clone();
        eve_doc.visibility = Visibility::private([]);
        let e1 = eve_identity
@@ -1424,6 +1512,7 @@ mod test {
        let bob = MockSigner::new(&mut rng);
        let eve = MockSigner::new(&mut rng);

+
        let mut cache = cob::cache::InMemory::default();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
        let (id, _, _, _) =
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
@@ -1433,7 +1522,8 @@ mod test {
        rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap();

        let repo = storage.repository(id).unwrap();
-
        let mut identity = Identity::load_mut(&repo).unwrap();
+
        let mut identities = Identities::open(&repo).unwrap();
+
        let mut identity = identities.load_mut(&mut cache).unwrap();
        let mut doc = identity.doc().clone();
        let prj = doc.project().unwrap();

@@ -1467,7 +1557,7 @@ mod test {
            .unwrap();
        identity.accept(&revision, &eve).unwrap();

-
        let identity: Identity = Identity::load(&repo).unwrap();
+
        let identity: Identity = identities.load().unwrap();
        let root = repo.identity_root().unwrap();
        let doc = repo.identity_doc_at(revision).unwrap();

modified radicle/src/identity/doc.rs
@@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::canonical::formatter::CanonicalFormatter;
+
use crate::cob;
use crate::cob::identity;
use crate::crypto;
use crate::crypto::{Signature, Unverified, Verified};
@@ -354,12 +355,18 @@ impl Doc<Verified> {
        Doc::from_json(blob.content())?.verified()
    }

-
    pub fn init<G: crypto::Signer>(
+
    pub fn init<G, C>(
        &self,
        repo: &storage::git::Repository,
+
        cache: &mut C,
        signer: &G,
-
    ) -> Result<git::Oid, RepositoryError> {
-
        let cob = identity::Identity::initialize(self, repo, signer)?;
+
    ) -> Result<git::Oid, RepositoryError>
+
    where
+
        C: cob::cache::Update<identity::Identity>,
+
        G: crypto::Signer,
+
    {
+
        let mut store = identity::Identities::open(repo)?;
+
        let cob = store.initialize(self, cache, signer)?;
        let id_ref = git::refs::storage::id(signer.public_key());
        let cob_ref = git::refs::storage::cob(
            signer.public_key(),
@@ -462,6 +469,7 @@ mod test {
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
        let (id, _, _) = rad::init(
            &repo,
+
            &mut cob::cache::NoCache,
            "heartwood",
            "Radicle Heartwood Protocol & Stack",
            git::refname!("master"),
@@ -509,6 +517,7 @@ mod test {
        let delegate = MockSigner::from_seed([0xff; 32]);
        let (rid, doc, _) = rad::init(
            &working,
+
            &mut cob::cache::NoCache,
            "heartwood",
            "Radicle Heartwood Protocol & Stack",
            git::refname!("master"),
modified radicle/src/profile.rs
@@ -543,6 +543,40 @@ impl Home {
        let store = cob::patch::Patches::open(repository)?;
        Ok(cob::patch::Cache::open(store, db))
    }
+

+
    /// Return a read-only handle for the identities cache.
+
    pub fn identities<'a, R>(
+
        &self,
+
        repository: &'a R,
+
    ) -> Result<
+
        cob::identity::Cache<cob::identity::Identities<'a, R>, cob::cache::StoreReader>,
+
        Error,
+
    >
+
    where
+
        R: ReadRepository + cob::Store,
+
    {
+
        let path = self.cobs().join(cob::cache::COBS_DB_FILE);
+
        let db = cob::cache::Store::reader(path)?;
+
        let store = cob::identity::Identities::open(repository)?;
+
        Ok(cob::identity::Cache::reader(store, db))
+
    }
+

+
    /// Return a read-write handle for the identities cache.
+
    pub fn identities_mut<'a, R>(
+
        &self,
+
        repository: &'a R,
+
    ) -> Result<
+
        cob::identity::Cache<cob::identity::Identities<'a, R>, cob::cache::StoreWriter>,
+
        Error,
+
    >
+
    where
+
        R: ReadRepository + cob::Store,
+
    {
+
        let path = self.cobs().join(cob::cache::COBS_DB_FILE);
+
        let db = cob::cache::Store::open(path)?;
+
        let store = cob::identity::Identities::open(repository)?;
+
        Ok(cob::identity::Cache::open(store, db))
+
    }
}

// Private methods.
modified radicle/src/rad.rs
@@ -6,7 +6,7 @@ use std::str::FromStr;
use once_cell::sync::Lazy;
use thiserror::Error;

-
use crate::cob::ObjectId;
+
use crate::cob::{cache, ObjectId};
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::doc;
@@ -44,15 +44,21 @@ pub enum InitError {
}

/// Initialize a new radicle project from a git repository.
-
pub fn init<G: Signer, S: WriteStorage>(
+
pub fn init<G, S, C>(
    repo: &git2::Repository,
+
    cache: &mut C,
    name: &str,
    description: &str,
    default_branch: BranchName,
    visibility: Visibility,
    signer: &G,
    storage: S,
-
) -> Result<(RepoId, identity::Doc<Verified>, SignedRefs<Verified>), InitError> {
+
) -> Result<(RepoId, identity::Doc<Verified>, SignedRefs<Verified>), InitError>
+
where
+
    G: Signer,
+
    S: WriteStorage,
+
    C: cache::Update<identity::Identity>,
+
{
    // TODO: Better error when project id already exists in storage, but remote doesn't.
    let pk = signer.public_key();
    let delegate = identity::Did::from(*pk);
@@ -70,7 +76,7 @@ pub fn init<G: Signer, S: WriteStorage>(
        )
    })?;
    let doc = identity::Doc::initial(proj, delegate, visibility).verified()?;
-
    let (project, _) = Repository::init(&doc, &storage, signer)?;
+
    let (project, _) = Repository::init(&doc, &storage, cache, signer)?;
    let url = git::Url::from(project.id);

    match init_configure(repo, &project, pk, &default_branch, &url, signer) {
@@ -388,6 +394,7 @@ mod tests {
    use pretty_assertions::assert_eq;
    use radicle_crypto::test::signer::MockSigner;

+
    use crate::cob;
    use crate::git::{name::component, qualified};
    use crate::identity::Did;
    use crate::storage::git::transport;
@@ -409,6 +416,7 @@ mod tests {
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
        let (proj, _, refs) = init(
            &repo,
+
            &mut cob::cache::NoCache,
            "acme",
            "Acme's repo",
            git::refname!("master"),
@@ -464,6 +472,7 @@ mod tests {
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
        let (id, _, alice_refs) = init(
            &original,
+
            &mut cob::cache::NoCache,
            "acme",
            "Acme's repo",
            git::refname!("master"),
@@ -500,6 +509,7 @@ mod tests {
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
        let (id, _, _) = init(
            &original,
+
            &mut cob::cache::NoCache,
            "acme",
            "Acme's repo",
            git::refname!("master"),
modified radicle/src/storage.rs
@@ -14,7 +14,8 @@ use crypto::{PublicKey, Signer, Unverified, Verified};
pub use git::{Validation, Validations};
pub use radicle_git_ext::Oid;

-
use crate::cob;
+
use crate::cob::identity::Identities;
+
use crate::cob::{self, identity};
use crate::collections::RandomMap;
use crate::git::ext as git_ext;
use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, RefString};
@@ -103,6 +104,8 @@ pub enum RepositoryError {
    Quorum(#[from] git::QuorumError),
    #[error(transparent)]
    Refs(#[from] refs::Error),
+
    #[error(transparent)]
+
    Identity(#[from] identity::Error),
}

/// Storage error.
@@ -467,7 +470,7 @@ pub trait ReadRepository: Sized + ValidateRepository {
    where
        Self: cob::Store,
    {
-
        Identity::load(self)
+
        Identities::open(self)?.load()
    }

    /// Compute the canonical `rad/id` of this repository.
modified radicle/src/storage/git.rs
@@ -11,6 +11,7 @@ use crypto::{Signer, Verified};
use once_cell::sync::Lazy;
use tempfile::TempDir;

+
use crate::cob::{cache, identity};
use crate::crypto::Unverified;
use crate::git;
use crate::identity::doc::DocError;
@@ -438,16 +439,21 @@ impl Repository {
    }

    /// Create the repository's identity branch.
-
    pub fn init<G: Signer, S: WriteStorage>(
+
    pub fn init<G, S, C>(
        doc: &Doc<Verified>,
        storage: &S,
+
        cache: &mut C,
        signer: &G,
-
    ) -> Result<(Self, git::Oid), RepositoryError> {
+
    ) -> Result<(Self, git::Oid), RepositoryError>
+
    where
+
        G: Signer,
+
        S: WriteStorage,
+
        C: cache::Update<Identity>,
+
    {
        let (doc_oid, _) = doc.encode()?;
        let id = RepoId::from(doc_oid);
        let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
-
        let commit = doc.init(&repo, signer)?;
-

+
        let commit = doc.init(&repo, cache, signer)?;
        Ok((repo, commit))
    }

@@ -800,7 +806,8 @@ impl ReadRepository for Repository {

            // We've got an identity that goes back to the correct root.
            if blob.id() == **self.id {
-
                let identity = Identity::get(&root.into(), self)?;
+
                let store = identity::Identities::open(self)?;
+
                let identity = store.get(&root.into())?;

                return Ok(identity.head());
            }
modified radicle/src/test/fixtures.rs
@@ -2,7 +2,6 @@ use std::path::Path;
use std::str::FromStr;

use crate::crypto::{PublicKey, Signer, Verified};
-
use crate::git;
use crate::identity::doc::Visibility;
use crate::identity::RepoId;
use crate::node::Alias;
@@ -10,6 +9,7 @@ use crate::rad;
use crate::storage::git::transport;
use crate::storage::git::Storage;
use crate::storage::refs::SignedRefs;
+
use crate::{cob, git};

/// The birth of the radicle project, January 1st, 2018.
pub const RADICLE_EPOCH: i64 = 1514817556;
@@ -38,6 +38,7 @@ pub fn storage<P: AsRef<Path>, G: Signer>(path: P, signer: &G) -> Result<Storage
        let (repo, _) = repository(path.join("workdir").join(name));
        rad::init(
            &repo,
+
            &mut cob::cache::NoCache,
            name,
            desc,
            git::refname!("master"),
@@ -61,6 +62,7 @@ pub fn project<P: AsRef<Path>, G: Signer>(
    let (working, head) = repository(path);
    let (id, _, refs) = rad::init(
        &working,
+
        &mut cob::cache::NoCache,
        "acme",
        "Acme's repository",
        git::refname!("master"),