pub(crate) use super::v7::{Commit, RepoStatus};
use crate::db::types::DidKey;
use jacquard_common::CowStr;
use jacquard_common::types::string::Handle;
use serde::{Deserialize, Serialize};
/// v7's row plus when its identity was last resolved. rows are msgpack arrays, so a v7 row
/// decodes as this one with the new field `None`, and nothing gets rewritten
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "'i: 'de"))]
pub(crate) struct RepoState<'i> {
pub active: bool,
pub status: RepoStatus,
pub root: Option<Commit>,
pub last_message_time: Option<i64>,
pub last_identity_time: Option<i64>,
pub last_account_time: Option<i64>,
pub last_updated_at: i64, // unix timestamp
#[serde(borrow)]
pub signing_key: Option<DidKey<'i>>,
#[serde(borrow)]
pub pds: Option<CowStr<'i>>,
pub handle: Option<Handle>,
/// unix secs of the doc fetch that set handle, pds and key. `None` on rows from before v15,
/// which read as resolved somewhere in the ttl before the db first opened at v15. it's the
/// last field and left out when `None`, so a row nobody stamped yet stays a v7 row
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity_resolved_at: Option<i64>,
}
#[cfg(test)]
mod tests {
use super::*;
fn v7_row() -> super::super::v7::RepoState<'static> {
super::super::v7::RepoState {
active: true,
status: RepoStatus::Synced,
root: None,
last_message_time: Some(7),
last_identity_time: Some(8),
last_account_time: Some(9),
last_updated_at: 42,
signing_key: Some(DidKey(std::borrow::Cow::Borrowed(&[0xed, 1, 2, 3]))),
pds: Some("https://pds.example".into()),
handle: Some(Handle::new_static("alice.test").unwrap()),
}
}
#[test]
fn v15_persisted_schema_golden_fixture() {
let old = v7_row();
let state = RepoState {
active: old.active,
status: old.status,
root: old.root,
last_message_time: old.last_message_time,
last_identity_time: old.last_identity_time,
last_account_time: old.last_account_time,
last_updated_at: old.last_updated_at,
signing_key: old.signing_key,
pds: old.pds,
handle: old.handle,
identity_resolved_at: Some(1_700_000_000),
};
let encoded = rmp_serde::to_vec(&state).unwrap();
assert_eq!(
crate::util::hex::encode(&encoded),
"9bc3a653796e636564c00708092ac404ed010203b368747470733a2f2f7064732e6578616d706c65aa616c6963652e74657374ce6553f100"
);
let decoded: RepoState<'_> = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(decoded.identity_resolved_at, Some(1_700_000_000));
assert_eq!(decoded.pds.as_deref(), Some("https://pds.example"));
}
#[test]
fn an_unstamped_row_is_written_as_a_v7_row() {
let old = v7_row();
let state = RepoState {
active: old.active,
status: old.status.clone(),
root: old.root.clone(),
last_message_time: old.last_message_time,
last_identity_time: old.last_identity_time,
last_account_time: old.last_account_time,
last_updated_at: old.last_updated_at,
signing_key: old.signing_key.clone(),
pds: old.pds.clone(),
handle: old.handle.clone(),
identity_resolved_at: None,
};
assert_eq!(
rmp_serde::to_vec(&state).unwrap(),
rmp_serde::to_vec(&old).unwrap()
);
}
#[test]
fn a_v7_row_decodes_with_no_resolve_time() {
let encoded = rmp_serde::to_vec(&v7_row()).unwrap();
let decoded: RepoState<'_> = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(decoded.identity_resolved_at, None);
assert_eq!(decoded.last_account_time, Some(9));
assert_eq!(
decoded.handle.as_ref().map(Handle::as_str),
Some("alice.test")
);
}
/// why v15 needs a fence: an older build can't read a row once it's stamped
#[test]
fn a_v7_reader_rejects_a_stamped_v15_row() {
let mut encoded = rmp_serde::to_vec(&v7_row()).unwrap();
encoded[0] = 0x9b;
encoded.extend_from_slice(&[0xce, 0x65, 0x53, 0xf1, 0x00]);
assert!(rmp_serde::from_slice::<RepoState<'_>>(&encoded).is_ok());
assert!(rmp_serde::from_slice::<super::super::v7::RepoState<'_>>(&encoded).is_err());
}
}