// Copyright © 2023 Kim Altintop <kim@eagain.io>
// SPDX-License-Identifier: GPL-2.0-only
use std::{
path::Path,
sync::Arc,
};
use automerge::ChangeHash;
use libpijul::{
pristine::L64,
small_string::{
SmallStr,
SmallString,
},
};
use sanakirja::{
btree::{
self,
Db,
UDb,
},
direct_repr,
RootDb,
Storable,
UnsizedStorable,
};
use thiserror::Error;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(usize)]
pub enum Root {
Version,
Docs,
}
const VERSION: L64 = L64(1u64.to_le());
#[derive(Debug, Error)]
pub enum Error {
#[error("already exists: {0}")]
AlreadyExists(String),
#[error("version mismatch")]
Version,
#[error("missing root: {0:?}")]
MissingRoot(Root),
#[error(transparent)]
Sanakirja(#[from] sanakirja::Error),
}
pub struct Sanakirja {
env: Arc<sanakirja::Env>,
}
impl Sanakirja {
pub fn new(name: Option<&Path>) -> Result<Self, Error> {
let sz = 1 << 20;
let roots = 2;
match name {
None => sanakirja::Env::new_anon(sz, roots),
Some(name) => sanakirja::Env::new(name, sz, roots),
}
.map(Arc::new)
.map(|env| Self { env })
.map_err(Into::into)
}
pub fn txn_begin(&self) -> Result<Txn, Error> {
let txn = sanakirja::Env::txn_begin(self.env.clone())?;
if L64(txn.root(Root::Version as usize)) != VERSION {
return Err(Error::Version);
}
let docs = txn
.root_db(Root::Docs as usize)
.ok_or_else(|| Error::MissingRoot(Root::Docs))?;
Ok(Txn { docs, txn })
}
pub fn mut_txn_begin(&self) -> Result<MutTxn<()>, Error> {
let mut txn = sanakirja::Env::mut_txn_begin(self.env.clone())?;
if let Some(version) = txn.root(Root::Version as usize) {
if L64(version) != VERSION {
return Err(Error::Version);
}
} else {
txn.set_root(Root::Version as usize, VERSION.0);
}
let docs = txn
.root_db(Root::Docs as usize)
.map_or_else(|| btree::create_db_(&mut txn), Ok)?;
Ok(MutTxn { docs, txn })
}
}
pub struct Doc {
changes: Db<L64, StoredHash>,
}
impl Doc {
pub fn changes<T>(&self, txn: &GenericTxn<T>) -> Result<Vec<(u64, ChangeHash)>, Error>
where
T: sanakirja::LoadPage<Error = sanakirja::Error> + sanakirja::RootPage,
{
let mut changes = Vec::new();
for x in btree::iter(&txn.txn, &self.changes, None)? {
let (t, hash) = x?;
changes.push((t.0, (*hash).into()));
}
Ok(changes)
}
pub fn put_change<T>(&mut self, txn: &mut MutTxn<T>, change: ChangeHash) -> Result<(), Error> {
let last = if let Some(max) = btree::rev_iter(&txn.txn, &self.changes, None)?.next() {
let (L64(x), _) = max?;
*x
} else {
0
};
btree::put(
&mut txn.txn,
&mut self.changes,
&L64(last + 1),
&change.into(),
)?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(C)]
struct StoredDoc {
changes: L64,
}
direct_repr!(StoredDoc);
impl sanakirja::debug::Check for StoredDoc {}
// TODO:
// * Versioning?
// * Ask automerge to export HASH_SIZE
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(C)]
struct StoredHash([u8; 32]);
direct_repr!(StoredHash);
impl sanakirja::debug::Check for StoredHash {}
impl From<ChangeHash> for StoredHash {
fn from(ChangeHash(h): ChangeHash) -> Self {
Self(h)
}
}
impl From<StoredHash> for ChangeHash {
fn from(StoredHash(h): StoredHash) -> Self {
Self(h)
}
}
pub type Txn = GenericTxn<sanakirja::Txn<Arc<sanakirja::Env>>>;
pub type MutTxn<T> = GenericTxn<sanakirja::MutTxn<Arc<sanakirja::Env>, T>>;
pub struct GenericTxn<T> {
txn: T,
// All documents, by name
docs: UDb<SmallStr, StoredDoc>,
}
impl MutTxn<()> {
pub fn get_doc(&mut self, name: &str) -> Result<Option<Doc>, Error> {
let name = SmallString::from_str(name);
let doc = match btree::get(&self.txn, &self.docs, &name, None)? {
Some((k, v)) if k == name.as_ref() => Some(Doc {
changes: Db::from_page(v.changes.into()),
}),
_ => None,
};
Ok(doc)
}
pub fn create_doc(&mut self, name: &str) -> Result<Doc, Error> {
let name = SmallString::from_str(name);
if let Some((k, _)) = btree::get(&self.txn, &self.docs, &name, None)? {
if k == name.as_ref() {
return Err(Error::AlreadyExists(name.as_str().to_owned()));
}
}
let doc = Doc {
changes: btree::create_db(&mut self.txn)?,
};
btree::del(&mut self.txn, &mut self.docs, &name, None)?;
let stored = StoredDoc {
changes: doc.changes.db.into(),
};
btree::put(&mut self.txn, &mut self.docs, &name, &stored)?;
Ok(doc)
}
}