use anyhow::Result;
use async_trait::async_trait;
use chrono::Utc;
use rocksdb::{DB, Direction, IteratorMode, ReadOptions, WriteBatch};
use uuid::Uuid;
use crate::traits::{CardStorage, CardWrite};
use post_cortex_core::core::card::{Card, CardScope};
use super::RealRocksDBStorage;
use super::core::{blocking, keys_with_prefix};
fn card_prefix(scope: CardScope) -> String {
format!("card:{}:", scope.key())
}
fn card_key(scope: CardScope, version: u64) -> String {
format!("card:{}:{:020}", scope.key(), version)
}
fn head_key(scope: CardScope) -> String {
format!("card_head:{}", scope.key())
}
fn lock_id(scope: CardScope) -> Uuid {
match scope {
CardScope::Workspace(id) => id,
CardScope::User => Uuid::nil(),
}
}
fn encode_card(card: &Card) -> Result<Vec<u8>> {
bincode::serde::encode_to_vec(card, bincode::config::standard())
.map_err(|e| anyhow::anyhow!("Failed to serialize card: {}", e))
}
fn decode_card(bytes: &[u8]) -> Result<Card> {
bincode::serde::decode_from_slice::<Card, _>(bytes, bincode::config::standard())
.map(|(c, _)| c)
.map_err(|e| anyhow::anyhow!("Failed to deserialize card: {}", e))
}
fn head(db: &DB, scope: CardScope) -> Result<u64> {
match db.get(head_key(scope).as_bytes())? {
Some(bytes) => {
let raw: [u8; 8] = bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Malformed card head ({} bytes)", bytes.len()))?;
Ok(u64::from_be_bytes(raw))
}
None => Ok(0),
}
}
fn load(db: &DB, scope: CardScope, version: u64) -> Result<Option<Card>> {
if version == 0 {
return Ok(None);
}
db.get(card_key(scope, version).as_bytes())?
.map(|bytes| decode_card(&bytes))
.transpose()
}
pub(super) fn delete_card_in(db: &DB, batch: &mut WriteBatch, scope: CardScope) -> Result<()> {
batch.delete(head_key(scope).as_bytes());
for key in keys_with_prefix(db, card_prefix(scope).as_bytes())? {
batch.delete(&key);
}
Ok(())
}
#[async_trait]
impl CardStorage for RealRocksDBStorage {
async fn get_card(&self, scope: CardScope) -> Result<Option<Card>> {
let db = self.db.clone();
blocking(move || load(&db, scope, head(&db, scope)?)).await
}
async fn put_card(
&self,
scope: CardScope,
text: String,
author: Option<String>,
expected_version: u64,
) -> Result<CardWrite> {
let db = self.db.clone();
let locks = self.session_locks.clone();
blocking(move || {
let _guard = locks.lock(lock_id(scope));
let latest = head(&db, scope)?;
if latest != expected_version {
return Ok(CardWrite::Conflict(load(&db, scope, latest)?));
}
let card = Card {
scope,
version: latest + 1,
text,
updated_at: Utc::now(),
author,
};
let mut batch = WriteBatch::default();
batch.put(
card_key(scope, card.version).as_bytes(),
encode_card(&card)?,
);
batch.put(head_key(scope).as_bytes(), card.version.to_be_bytes());
db.write(batch)?;
Ok(CardWrite::Written(card))
})
.await
}
async fn card_history(&self, scope: CardScope, limit: usize) -> Result<Vec<Card>> {
let db = self.db.clone();
blocking(move || {
let prefix = card_prefix(scope);
let mut opts = ReadOptions::default();
opts.set_total_order_seek(true);
opts.set_iterate_lower_bound(prefix.clone().into_bytes());
let mut upper = prefix.clone().into_bytes();
upper.push(0xFF);
let mut cards = Vec::new();
for item in db.iterator_opt(IteratorMode::From(&upper, Direction::Reverse), opts) {
if cards.len() >= limit {
break;
}
let (key, value) = item?;
if !key.starts_with(prefix.as_bytes()) {
break;
}
cards.push(decode_card(&value)?);
}
Ok(cards)
})
.await
}
}