···55//! which are DISTINCT from repository commit objects.
6677use bytes::Bytes;
88-use jacquard_common::IntoStatic;
99-use jacquard_common::types::string::{Did, Tid};
88+use jacquard_common::types::cid::CidLink;
99+use jacquard_common::types::crypto::PublicKey;
1010+use jacquard_common::types::string::{Datetime, Did, Tid};
1111+use jacquard_common::{CowStr, IntoStatic};
10121113/// Firehose commit message (sync v1.0 and v1.1)
1214///
···3335 pub since: Tid,
34363537 /// Timestamp of when this message was originally broadcast
3636- pub time: jacquard_common::types::string::Datetime,
3838+ pub time: Datetime,
37393840 /// Repo commit object CID
3941 ///
4042 /// This CID points to the repository commit block (with did, version, data, rev, prev, sig).
4143 /// It must be the first entry in the CAR header 'roots' list.
4244 #[serde(borrow)]
4343- pub commit: jacquard_common::types::cid::CidLink<'a>,
4545+ pub commit: CidLink<'a>,
44464547 /// CAR file containing relevant blocks
4648 ///
···7072 /// - Consumers must have previous repository state
7173 #[serde(skip_serializing_if = "Option::is_none")]
7274 #[serde(borrow)]
7373- pub prev_data: Option<jacquard_common::types::cid::CidLink<'a>>,
7575+ pub prev_data: Option<CidLink<'a>>,
74767577 /// Blob CIDs referenced in this commit
7678 #[serde(borrow)]
7777- pub blobs: Vec<jacquard_common::types::cid::CidLink<'a>>,
7979+ pub blobs: Vec<CidLink<'a>>,
78807981 /// DEPRECATED: Replaced by #sync event and data limits
8082 ///
···9294pub struct RepoOp<'a> {
9395 /// Operation type: "create", "update", or "delete"
9496 #[serde(borrow)]
9595- pub action: jacquard_common::CowStr<'a>,
9797+ pub action: CowStr<'a>,
96989799 /// Collection/rkey path (e.g., "app.bsky.feed.post/abc123")
98100 #[serde(borrow)]
9999- pub path: jacquard_common::CowStr<'a>,
101101+ pub path: CowStr<'a>,
100102101103 /// For creates and updates, the new record CID. For deletions, None (null).
102104 #[serde(skip_serializing_if = "Option::is_none")]
103105 #[serde(borrow)]
104104- pub cid: Option<jacquard_common::types::cid::CidLink<'a>>,
106106+ pub cid: Option<CidLink<'a>>,
105107106108 /// For updates and deletes, the previous record CID
107109 ///
···109111 /// For creates, this field should not be defined.
110112 #[serde(skip_serializing_if = "Option::is_none")]
111113 #[serde(borrow)]
112112- pub prev: Option<jacquard_common::types::cid::CidLink<'a>>,
114114+ pub prev: Option<CidLink<'a>>,
113115}
114116115117impl IntoStatic for FirehoseCommit<'_> {
···146148 }
147149}
148150151151+use crate::car::parse_car_bytes;
149152/// Validation functions for firehose commit messages
150153///
151154/// These functions validate commits from the `com.atproto.sync.subscribeRepos` firehose.
152152-use crate::error::Result;
155155+use crate::error::{RepoError, Result};
153156use crate::mst::Mst;
154154-use crate::storage::BlockStore;
157157+use crate::storage::{BlockStore, LayeredBlockStore, MemoryBlockStore};
155158use cid::Cid as IpldCid;
156159use std::sync::Arc;
157160···173176 &self,
174177 prev_mst_root: Option<IpldCid>,
175178 prev_storage: Arc<S>,
176176- pubkey: &jacquard_common::types::crypto::PublicKey<'_>,
179179+ pubkey: &PublicKey<'_>,
177180 ) -> Result<IpldCid> {
178181 // 1. Parse CAR blocks from the firehose message into temporary storage
179179- let parsed = crate::car::parse_car_bytes(&self.blocks).await?;
180180- let temp_storage = crate::storage::MemoryBlockStore::new_from_blocks(parsed.blocks);
182182+ let parsed = parse_car_bytes(&self.blocks).await?;
183183+ let temp_storage = MemoryBlockStore::new_from_blocks(parsed.blocks);
181184182185 // 2. Create layered storage: reads from temp first, then prev; writes to temp only
183186 // This avoids copying all previous MST blocks
184184- let layered_storage =
185185- crate::storage::LayeredBlockStore::new(temp_storage.clone(), prev_storage);
187187+ let layered_storage = LayeredBlockStore::new(temp_storage.clone(), prev_storage);
186188187189 // 3. Extract and verify commit object from temporary storage
188190 let commit_cid: IpldCid = self
189191 .commit
190192 .to_ipld()
191191- .map_err(|e| crate::error::RepoError::invalid(format!("Invalid commit CID: {}", e)))?;
193193+ .map_err(|e| RepoError::invalid(format!("Invalid commit CID: {}", e)))?;
192194 let commit_bytes = temp_storage
193195 .get(&commit_cid)
194196 .await?
195195- .ok_or_else(|| crate::error::RepoError::not_found("commit block", &commit_cid))?;
197197+ .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?;
196198197199 let commit = super::Commit::from_cbor(&commit_bytes)?;
198200199201 // Verify DID matches
200202 if commit.did().as_ref() != self.repo.as_ref() {
201201- return Err(crate::error::RepoError::invalid_commit(format!(
203203+ return Err(RepoError::invalid_commit(format!(
202204 "DID mismatch: commit has {}, message has {}",
203205 commit.did(),
204206 self.repo
···232234 let computed_root = computed_mst.get_pointer().await?;
233235234236 if computed_root != expected_root {
235235- return Err(crate::error::RepoError::invalid_commit(format!(
237237+ return Err(RepoError::invalid_commit(format!(
236238 "MST root mismatch: expected {}, got {}",
237239 expected_root, computed_root
238240 )));
···259261 /// **Inductive property:** Can validate without any external state besides the blocks
260262 /// in this message. The `prev_data` field provides the starting MST root, and operations
261263 /// include `prev` CIDs for validation. All necessary blocks must be in the CAR bytes.
262262- pub async fn validate_v1_1(
263263- &self,
264264- pubkey: &jacquard_common::types::crypto::PublicKey<'_>,
265265- ) -> Result<IpldCid> {
264264+ pub async fn validate_v1_1(&self, pubkey: &PublicKey<'_>) -> Result<IpldCid> {
266265 // 1. Require prev_data for v1.1
267266 let prev_data_cid: IpldCid = self
268267 .prev_data
269268 .as_ref()
270269 .ok_or_else(|| {
271271- crate::error::RepoError::invalid_commit(
272272- "Sync v1.1 validation requires prev_data field",
273273- )
270270+ RepoError::invalid_commit("Sync v1.1 validation requires prev_data field")
274271 })?
275272 .to_ipld()
276276- .map_err(|e| {
277277- crate::error::RepoError::invalid(format!("Invalid prev_data CID: {}", e))
278278- })?;
273273+ .map_err(|e| RepoError::invalid(format!("Invalid prev_data CID: {}", e)))?;
279274280275 // 2. Parse CAR blocks from the firehose message into temporary storage
281281- let parsed = crate::car::parse_car_bytes(&self.blocks).await?;
282282- let temp_storage = Arc::new(crate::storage::MemoryBlockStore::new_from_blocks(
283283- parsed.blocks,
284284- ));
276276+ let parsed = parse_car_bytes(&self.blocks).await?;
277277+ let temp_storage = Arc::new(MemoryBlockStore::new_from_blocks(parsed.blocks));
285278286279 // 3. Extract and verify commit object from temporary storage
287280 let commit_cid: IpldCid = self
288281 .commit
289282 .to_ipld()
290290- .map_err(|e| crate::error::RepoError::invalid(format!("Invalid commit CID: {}", e)))?;
283283+ .map_err(|e| RepoError::invalid(format!("Invalid commit CID: {}", e)))?;
291284 let commit_bytes = temp_storage
292285 .get(&commit_cid)
293286 .await?
294294- .ok_or_else(|| crate::error::RepoError::not_found("commit block", &commit_cid))?;
287287+ .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?;
295288296289 let commit = super::Commit::from_cbor(&commit_bytes)?;
297290298291 // Verify DID matches
299292 if commit.did().as_ref() != self.repo.as_ref() {
300300- return Err(crate::error::RepoError::invalid_commit(format!(
293293+ return Err(RepoError::invalid_commit(format!(
301294 "DID mismatch: commit has {}, message has {}",
302295 commit.did(),
303296 self.repo
···325318 let computed_root = computed_mst.get_pointer().await?;
326319327320 if computed_root != expected_root {
328328- return Err(crate::error::RepoError::invalid_commit(format!(
321321+ return Err(RepoError::invalid_commit(format!(
329322 "MST root mismatch: expected {}, got {}",
330323 expected_root, computed_root
331324 )));
+3-2
crates/jacquard-repo/src/commit/proof.rs
···2222use crate::mst::Mst;
2323use crate::storage::MemoryBlockStore;
2424use cid::Cid as IpldCid;
2525+use jacquard_common::CowStr;
2526use jacquard_common::types::string::Did;
2627use smol_str::format_smolstr;
2728use std::sync::Arc;
···3031#[derive(Debug, Clone, PartialEq, Eq)]
3132pub struct RecordClaim<'a> {
3233 /// Collection NSID (e.g., "app.bsky.feed.post")
3333- pub collection: jacquard_common::CowStr<'a>,
3434+ pub collection: CowStr<'a>,
34353536 /// Record key (TID or other identifier)
3636- pub rkey: jacquard_common::CowStr<'a>,
3737+ pub rkey: CowStr<'a>,
37383839 /// Expected CID of the record
3940 /// - Some(cid): claiming record exists with this CID
+1-1
crates/jacquard-repo/src/error.rs
···269269}
270270271271/// Diff-specific errors
272272-#[derive(Debug, thiserror::Error)]
272272+#[derive(Debug, thiserror::Error, miette::Diagnostic)]
273273pub enum DiffError {
274274 /// Too many operations
275275 #[error("Too many operations: {count} (max {max})")]
+18-23
crates/jacquard-repo/src/mst/diff.rs
···11//! MST diff calculation
2233use std::collections::BTreeMap;
44+use std::future::Future;
55+use std::pin::Pin;
4657use super::cursor::{CursorPosition, MstCursor};
68use super::tree::Mst;
77-use crate::error::Result;
99+use super::util::serialize_node_data;
1010+use crate::commit::firehose::RepoOp;
1111+use crate::error::{RepoError, Result};
1212+use crate::mst::NodeEntry;
813use crate::storage::BlockStore;
914use bytes::Bytes;
1015use cid::Cid as IpldCid;
1616+use jacquard_common::types::cid::CidLink;
1117use smol_str::SmolStr;
12181319/// Diff between two MST states
···8793 /// The sync protocol has a 200 operation limit per commit.
8894 pub fn validate_limits(&self) -> Result<()> {
8995 if self.op_count() > 200 {
9090- return Err(crate::error::RepoError::too_large(
9696+ return Err(RepoError::too_large(
9197 "diff operation count",
9298 self.op_count(),
9399 200,
···139145 &self,
140146 storage: &S,
141147 ) -> Result<std::collections::BTreeMap<IpldCid, bytes::Bytes>> {
142142- use std::collections::BTreeMap;
143143-144148 let mut blocks = BTreeMap::new();
145149146150 for cid in &self.new_leaf_cids {
···156160 ///
157161 /// Returns operations in the format used by `com.atproto.sync.subscribeRepos`.
158162 /// All update/delete operations include prev CIDs for sync v1.1 validation.
159159- pub fn to_repo_ops(&self) -> Vec<crate::commit::firehose::RepoOp<'_>> {
160160- use jacquard_common::types::cid::CidLink;
161161-163163+ pub fn to_repo_ops(&self) -> Vec<RepoOp<'_>> {
162164 let mut ops = Vec::with_capacity(self.op_count());
163165164166 // Add creates
165167 for (key, cid) in &self.creates {
166166- ops.push(crate::commit::firehose::RepoOp {
168168+ ops.push(RepoOp {
167169 action: "create".into(),
168170 path: key.as_str().into(),
169171 cid: Some(CidLink::from(*cid)),
···173175174176 // Add updates
175177 for (key, new_cid, old_cid) in &self.updates {
176176- ops.push(crate::commit::firehose::RepoOp {
178178+ ops.push(RepoOp {
177179 action: "update".into(),
178180 path: key.as_str().into(),
179181 cid: Some(CidLink::from(*new_cid)),
···183185184186 // Add deletes
185187 for (key, old_cid) in &self.deletes {
186186- ops.push(crate::commit::firehose::RepoOp {
188188+ ops.push(RepoOp {
187189 action: "delete".into(),
188190 path: key.as_str().into(),
189191 cid: None, // null for deletes
···223225 old: &'a Mst<S>,
224226 new: &'a Mst<S>,
225227 diff: &'a mut MstDiff,
226226-) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
228228+) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
227229 Box::pin(async move {
228230 // If CIDs are equal, trees are identical - skip entire subtree
229231 let old_cid = old.get_pointer().await?;
···406408407409 // Serialize the MST node
408410 let entries = tree.get_entries().await?;
409409- let node_data = super::util::serialize_node_data(&entries).await?;
410410- let cbor = serde_ipld_dagcbor::to_vec(&node_data)
411411- .map_err(|e| crate::error::RepoError::serialization(e))?;
411411+ let node_data = serialize_node_data(&entries).await?;
412412+ let cbor = serde_ipld_dagcbor::to_vec(&node_data).map_err(|e| RepoError::serialization(e))?;
412413413414 // Track the serialized block
414415 diff.new_mst_blocks.insert(tree_cid, Bytes::from(cbor));
···420421fn track_added_tree<'a, S: BlockStore + Sync + 'static>(
421422 tree: &'a Mst<S>,
422423 diff: &'a mut MstDiff,
423423-) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
424424+) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
424425 Box::pin(async move {
425425- use super::node::NodeEntry;
426426-427426 // Serialize and track this MST node
428427 serialize_and_track_mst(tree, diff).await?;
429428···448447fn track_removed_tree<'a, S: BlockStore + Sync + 'static>(
449448 tree: &'a Mst<S>,
450449 diff: &'a mut MstDiff,
451451-) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
450450+) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
452451 Box::pin(async move {
453453- use super::node::NodeEntry;
454454-455452 // Track this MST node as removed
456453 let tree_cid = tree.get_pointer().await?;
457454 diff.removed_mst_blocks.push(tree_cid);
···489486fn track_removed_tree_all<'a, S: BlockStore + Sync + 'static>(
490487 tree: &'a Mst<S>,
491488 diff: &'a mut MstDiff,
492492-) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
489489+) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
493490 Box::pin(async move {
494494- use super::node::NodeEntry;
495495-496491 // Track this node as removed
497492 let tree_cid = tree.get_pointer().await?;
498493 diff.removed_mst_blocks.push(tree_cid);
+6-4
crates/jacquard-repo/src/mst/node.rs
···66use cid::Cid as IpldCid;
77use smol_str::SmolStr;
8899+use crate::{mst::Mst, storage::BlockStore};
1010+911/// Entry in an MST node - either a subtree or a leaf
1012///
1113/// This is the in-memory representation used for tree operations.
···1416///
1517/// The wire format (CBOR) is different - see `NodeData` and `TreeEntry`.
1618#[derive(Clone)]
1717-pub enum NodeEntry<S: crate::storage::BlockStore> {
1919+pub enum NodeEntry<S> {
1820 /// Subtree reference
1921 ///
2022 /// Will be lazily loaded from storage when needed.
2121- Tree(crate::mst::Mst<S>),
2323+ Tree(Mst<S>),
22242325 /// Leaf node with key-value pair
2426 Leaf {
···2931 },
3032}
31333232-impl<S: crate::storage::BlockStore> fmt::Debug for NodeEntry<S> {
3434+impl<S: BlockStore> fmt::Debug for NodeEntry<S> {
3335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3436 match self {
3537 NodeEntry::Tree(t) => write!(f, "{:?}", t),
···4042 }
4143}
42444343-impl<S: crate::storage::BlockStore> NodeEntry<S> {
4545+impl<S: BlockStore> NodeEntry<S> {
4446 /// Check if this is a tree entry
4547 pub fn is_tree(&self) -> bool {
4648 matches!(self, NodeEntry::Tree(_))
···33//! Optional convenience layer over MST primitives. Provides type-safe record operations,
44//! batch writes, commit creation, and CAR export.
5566-use crate::commit::Commit;
77-use crate::error::Result;
88-use crate::mst::Mst;
66+use crate::commit::firehose::{FirehoseCommit, RepoOp};
77+use crate::commit::{Commit, SigningKey};
88+use crate::error::{RepoError, Result};
99+use crate::mst::{Mst, MstDiff, RecordWriteOp};
910use crate::storage::BlockStore;
1011use cid::Cid as IpldCid;
1112use jacquard_common::IntoStatic;
1212-use jacquard_common::types::string::{Did, Nsid, RecordKey, Tid};
1313+use jacquard_common::types::cid::CidLink;
1414+use jacquard_common::types::recordkey::RecordKeyType;
1515+use jacquard_common::types::string::{Datetime, Did, Nsid, RecordKey, Tid};
1316use jacquard_common::types::tid::Ticker;
1717+use smol_str::format_smolstr;
1418use std::collections::BTreeMap;
1519use std::path::Path;
1620use std::sync::Arc;
···3943 /// Previous MST root CID (for sync v1.1)
4044 pub prev_data: Option<IpldCid>,
41454242- /// All blocks to persist (MST nodes + record data + commit block)
4343- ///
4444- /// Includes:
4545- /// - All new MST node blocks from `diff.new_mst_blocks`
4646- /// - All new record data blocks (from creates + updates)
4747- /// - The commit block itself
4646+ /// New blocks to persist (MST nodes + record data + commit block)
4847 pub blocks: BTreeMap<IpldCid, bytes::Bytes>,
49485049 /// Relevant blocks for firehose (sync v1.1 inductive validation)
···5554 /// - Includes "adjacent" blocks needed for operation inversion
5655 pub relevant_blocks: BTreeMap<IpldCid, bytes::Bytes>,
57565858- /// CIDs of blocks to delete from storage
5959- ///
6060- /// Contains CIDs that are no longer referenced by the current tree:
6161- /// - Record CIDs from deleted records
6262- /// - Old record CIDs from updated records
6363- ///
6464- /// **Note:** Actual deletion should consider whether previous commits still
6565- /// reference these CIDs. A proper GC strategy might:
6666- /// - Only delete if previous commits are also being GC'd
6767- /// - Use reference counting across all retained commits
6868- /// - Perform periodic reachability analysis
6969- ///
7070- /// For simple single-commit repos or when old commits are discarded, direct
7171- /// deletion is safe.
5757+ /// CIDs of blocks to delete
7258 pub deleted_cids: Vec<IpldCid>,
7359}
7460···8167 &self,
8268 repo: &Did<'_>,
8369 seq: i64,
8484- time: jacquard_common::types::string::Datetime,
8585- ops: Vec<crate::commit::firehose::RepoOp<'static>>,
8686- blobs: Vec<jacquard_common::types::cid::CidLink<'static>>,
8787- ) -> Result<crate::commit::firehose::FirehoseCommit<'static>> {
8888- use jacquard_common::types::cid::CidLink;
8989-7070+ time: Datetime,
7171+ ops: Vec<RepoOp<'static>>,
7272+ blobs: Vec<CidLink<'static>>,
7373+ ) -> Result<FirehoseCommit<'static>> {
9074 // Convert relevant blocks to CAR format
9175 let blocks_car =
9276 crate::car::write_car_bytes(self.cid, self.relevant_blocks.clone()).await?;
93779494- Ok(crate::commit::firehose::FirehoseCommit {
7878+ Ok(FirehoseCommit {
9579 repo: repo.clone().into_static(),
9680 rev: self.rev.clone(),
9781 seq,
···161145 let commit_bytes = storage
162146 .get(commit_cid)
163147 .await?
164164- .ok_or_else(|| crate::error::RepoError::not_found("commit", commit_cid))?;
148148+ .ok_or_else(|| RepoError::not_found("commit", commit_cid))?;
165149166150 let commit = Commit::from_cbor(&commit_bytes)?;
167151 let mst_root = commit.data();
···177161 }
178162179163 /// Get a record by collection and rkey
180180- pub async fn get_record<T: jacquard_common::types::recordkey::RecordKeyType>(
164164+ pub async fn get_record<T: RecordKeyType>(
181165 &self,
182166 collection: &Nsid<'_>,
183167 rkey: &RecordKey<T>,
···187171 }
188172189173 /// Create a record (error if exists)
190190- pub async fn create_record<T: jacquard_common::types::recordkey::RecordKeyType>(
174174+ pub async fn create_record<T: RecordKeyType>(
191175 &mut self,
192176 collection: &Nsid<'_>,
193177 rkey: &RecordKey<T>,
···196180 let key = format!("{}/{}", collection.as_ref(), rkey.as_ref());
197181198182 if self.mst.get(&key).await?.is_some() {
199199- return Err(crate::error::RepoError::already_exists("record", &key));
183183+ return Err(RepoError::already_exists("record", &key));
200184 }
201185202186 self.mst = self.mst.add(&key, record_cid).await?;
···204188 }
205189206190 /// Update a record (error if not exists, returns previous CID)
207207- pub async fn update_record<T: jacquard_common::types::recordkey::RecordKeyType>(
191191+ pub async fn update_record<T: RecordKeyType>(
208192 &mut self,
209193 collection: &Nsid<'_>,
210194 rkey: &RecordKey<T>,
···216200 .mst
217201 .get(&key)
218202 .await?
219219- .ok_or_else(|| crate::error::RepoError::not_found("record", &key))?;
203203+ .ok_or_else(|| RepoError::not_found("record", &key))?;
220204221205 self.mst = self.mst.update(&key, record_cid).await?;
222206 Ok(old_cid)
223207 }
224208225209 /// Delete a record (error if not exists, returns deleted CID)
226226- pub async fn delete_record<T: jacquard_common::types::recordkey::RecordKeyType>(
210210+ pub async fn delete_record<T: RecordKeyType>(
227211 &mut self,
228212 collection: &Nsid<'_>,
229213 rkey: &RecordKey<T>,
···234218 .mst
235219 .get(&key)
236220 .await?
237237- .ok_or_else(|| crate::error::RepoError::not_found("record", &key))?;
221221+ .ok_or_else(|| RepoError::not_found("record", &key))?;
238222239223 self.mst = self.mst.delete(&key).await?;
240224 Ok(old_cid)
241225 }
242226227227+ // TODO(cursor-based queries): Potential future API additions
228228+ //
229229+ // The current API is purely single-record CRUD. Cursor-based traversal (see mst/cursor.rs)
230230+ // would enable efficient collection/range queries:
231231+ //
232232+ // - list_collection(collection: &Nsid) -> Vec<(RecordKey, IpldCid)>
233233+ // Enumerate all records in a collection via prefix search on "collection/"
234234+ // Uses cursor.advance() + cursor.skip_subtree() to skip irrelevant branches
235235+ //
236236+ // - list_collection_range(collection: &Nsid, start: &Rkey, end: &Rkey) -> Vec<...>
237237+ // Range query: advance to start key, collect until > end, skip subtrees outside range
238238+ // Useful for pagination / time-bounded queries (since Rkeys are often TIDs)
239239+ //
240240+ // - list_all_collections() -> Vec<Nsid>
241241+ // Walk tree, track collection prefixes, skip subtrees once prefix changes
242242+ //
243243+ // Current single-key get() is already optimal (O(log n) targeted lookup).
244244+ // But these bulk operations would benefit significantly from cursor's skip_subtree()
245245+ // to avoid traversing unrelated branches when searching lexicographically-organized data.
246246+243247 /// Apply record write operations with inline data
244248 ///
245249 /// Serializes record data to DAG-CBOR, computes CIDs, stores data blocks,
246250 /// then applies write operations to the MST. Returns the diff for inspection.
247251 ///
248252 /// For creating commits with operations, use `create_commit()` instead.
249249- pub async fn apply_record_writes(
250250- &mut self,
251251- ops: &[crate::mst::RecordWriteOp<'_>],
252252- ) -> Result<crate::mst::MstDiff> {
253253- use crate::mst::RecordWriteOp;
253253+ pub async fn apply_record_writes(&mut self, ops: &[RecordWriteOp<'_>]) -> Result<MstDiff> {
254254 use smol_str::format_smolstr;
255255256256 let mut updated_tree = self.mst.clone();
···266266267267 // Serialize record to DAG-CBOR
268268 let cbor = serde_ipld_dagcbor::to_vec(record)
269269- .map_err(|e| crate::error::RepoError::serialization(e))?;
269269+ .map_err(|e| RepoError::serialization(e))?;
270270271271 // Compute CID and store data
272272 let cid = self.storage.put(&cbor).await?;
···283283284284 // Serialize record to DAG-CBOR
285285 let cbor = serde_ipld_dagcbor::to_vec(record)
286286- .map_err(|e| crate::error::RepoError::serialization(e))?;
286286+ .map_err(|e| RepoError::serialization(e))?;
287287288288 // Compute CID and store data
289289 let cid = self.storage.put(&cbor).await?;
···291291 // Validate prev if provided
292292 if let Some(prev_cid) = prev {
293293 if &cid != prev_cid {
294294- return Err(crate::error::RepoError::invalid(format!(
294294+ return Err(RepoError::invalid(format!(
295295 "Update prev CID mismatch for key {}: expected {}, got {}",
296296 key, prev_cid, cid
297297 )));
···308308 let key = format_smolstr!("{}/{}", collection.as_ref(), rkey.as_ref());
309309310310 // Check exists
311311- let current = self.mst.get(key.as_str()).await?.ok_or_else(|| {
312312- crate::error::RepoError::not_found("record", key.as_str())
313313- })?;
311311+ let current = self
312312+ .mst
313313+ .get(key.as_str())
314314+ .await?
315315+ .ok_or_else(|| RepoError::not_found("record", key.as_str()))?;
314316315317 // Validate prev if provided
316318 if let Some(prev_cid) = prev {
317319 if ¤t != prev_cid {
318318- return Err(crate::error::RepoError::invalid(format!(
320320+ return Err(RepoError::invalid(format!(
319321 "Delete prev CID mismatch for key {}: expected {}, got {}",
320322 key, prev_cid, current
321323 )));
···347349 /// Returns `(ops, CommitData)` - ops are needed for `to_firehose_commit()`.
348350 pub async fn create_commit<K>(
349351 &mut self,
350350- ops: &[crate::mst::RecordWriteOp<'_>],
352352+ ops: &[RecordWriteOp<'_>],
351353 did: &Did<'_>,
352354 prev: Option<IpldCid>,
353355 signing_key: &K,
354354- ) -> Result<(Vec<crate::commit::firehose::RepoOp<'static>>, CommitData)>
356356+ ) -> Result<(Vec<RepoOp<'static>>, CommitData)>
355357 where
356356- K: crate::commit::SigningKey,
358358+ K: SigningKey,
357359 {
358358- use crate::mst::RecordWriteOp;
359359- use smol_str::format_smolstr;
360360-361360 // Step 1: Apply all write operations to build new MST
362361 let mut updated_tree = self.mst.clone();
363362···372371373372 // Serialize record to DAG-CBOR
374373 let cbor = serde_ipld_dagcbor::to_vec(record)
375375- .map_err(|e| crate::error::RepoError::serialization(e))?;
374374+ .map_err(|e| RepoError::serialization(e))?;
376375377376 // Compute CID and store data
378377 let cid = self.storage.put(&cbor).await?;
···389388390389 // Serialize record to DAG-CBOR
391390 let cbor = serde_ipld_dagcbor::to_vec(record)
392392- .map_err(|e| crate::error::RepoError::serialization(e))?;
391391+ .map_err(|e| RepoError::serialization(e))?;
393392394393 // Compute CID and store data
395394 let cid = self.storage.put(&cbor).await?;
···397396 // Validate prev if provided
398397 if let Some(prev_cid) = prev {
399398 if &cid != prev_cid {
400400- return Err(crate::error::RepoError::invalid(format!(
399399+ return Err(RepoError::invalid(format!(
401400 "Update prev CID mismatch for key {}: expected {}, got {}",
402401 key, prev_cid, cid
403402 )));
···414413 let key = format_smolstr!("{}/{}", collection.as_ref(), rkey.as_ref());
415414416415 // Check exists
417417- let current = self.mst.get(key.as_str()).await?.ok_or_else(|| {
418418- crate::error::RepoError::not_found("record", key.as_str())
419419- })?;
416416+ let current = self
417417+ .mst
418418+ .get(key.as_str())
419419+ .await?
420420+ .ok_or_else(|| RepoError::not_found("record", key.as_str()))?;
420421421422 // Validate prev if provided
422423 if let Some(prev_cid) = prev {
423424 if ¤t != prev_cid {
424424- return Err(crate::error::RepoError::invalid(format!(
425425+ return Err(RepoError::invalid(format!(
425426 "Delete prev CID mismatch for key {}: expected {}, got {}",
426427 key, prev_cid, current
427428 )));
···518519 .storage
519520 .get(&commit_cid)
520521 .await?
521521- .ok_or_else(|| crate::error::RepoError::not_found("commit block", &commit_cid))?;
522522+ .ok_or_else(|| RepoError::not_found("commit block", &commit_cid))?;
522523 let commit = Commit::from_cbor(&commit_bytes)?;
523524524525 self.commit = commit.into_static();
···540541 did: &Did<'_>,
541542 prev: Option<IpldCid>,
542543 signing_key: &K,
543543- ) -> Result<(Vec<crate::commit::firehose::RepoOp<'static>>, IpldCid)>
544544+ ) -> Result<(Vec<RepoOp<'static>>, IpldCid)>
544545 where
545545- K: crate::commit::SigningKey,
546546+ K: SigningKey,
546547 {
547548 let (ops, commit_data) = self.create_commit(&[], did, prev, signing_key).await?;
548549 Ok((ops, self.apply_commit(commit_data).await?))
···581582582583#[cfg(test)]
583584mod tests {
584584- use std::str::FromStr;
585585+ use std::{collections::BTreeMap, str::FromStr};
585586586587 use super::*;
587588 use crate::storage::MemoryBlockStore;
588588- use jacquard_common::types::recordkey::Rkey;
589589+ use jacquard_common::types::{
590590+ crypto::{KeyCodec, PublicKey},
591591+ recordkey::Rkey,
592592+ value::RawData,
593593+ };
589594 use smol_str::SmolStr;
590595591596 fn make_test_cid(value: u8) -> IpldCid {
···598603 IpldCid::new_v1(DAG_CBOR_CID_CODEC, mh)
599604 }
600605601601- fn make_test_record(
602602- n: u32,
603603- ) -> std::collections::BTreeMap<SmolStr, jacquard_common::types::value::RawData<'static>> {
604604- use jacquard_common::types::value::RawData;
605605- use smol_str::SmolStr;
606606-607607- let mut record = std::collections::BTreeMap::new();
606606+ fn make_test_record(n: u32) -> BTreeMap<SmolStr, RawData<'static>> {
607607+ let mut record = BTreeMap::new();
608608 record.insert(
609609 SmolStr::new("$type"),
610610 RawData::String("app.bsky.feed.post".into()),
···887887888888 #[tokio::test]
889889 async fn test_commit_signature_verification() {
890890- use jacquard_common::types::crypto::{KeyCodec, PublicKey};
891891-892890 let storage = Arc::new(MemoryBlockStore::new());
893891 let mut repo = create_test_repo(storage.clone()).await;
894892···1085108310861084 // Verify we can deserialize the record data
10871085 let record1_bytes = commit_data.blocks.get(&cid1).unwrap();
10881088- let record1: std::collections::BTreeMap<SmolStr, jacquard_common::types::value::RawData> =
10861086+ let record1: BTreeMap<SmolStr, RawData> =
10891087 serde_ipld_dagcbor::from_slice(record1_bytes).unwrap();
10901088 assert_eq!(
10911089 record1.get(&SmolStr::new("text")).unwrap(),
+11-6
crates/jacquard-repo/src/storage/layered.rs
···9696 self.base.has(cid).await
9797 }
98989999- async fn put_many(&self, blocks: impl IntoIterator<Item = (IpldCid, Bytes)> + Send) -> Result<()> {
9999+ async fn put_many(
100100+ &self,
101101+ blocks: impl IntoIterator<Item = (IpldCid, Bytes)> + Send,
102102+ ) -> Result<()> {
100103 // All writes go to writable layer
101104 self.writable.put_many(blocks).await
102105 }
···119122120123#[cfg(test)]
121124mod tests {
125125+ use std::sync::Arc;
126126+122127 use super::*;
123128 use crate::storage::MemoryBlockStore;
124129125130 #[tokio::test]
126131 async fn test_layered_read_from_writable() {
127127- let base = std::sync::Arc::new(MemoryBlockStore::new());
132132+ let base = Arc::new(MemoryBlockStore::new());
128133 let writable = MemoryBlockStore::new();
129134130135 // Put data in writable layer
···139144140145 #[tokio::test]
141146 async fn test_layered_fallback_to_base() {
142142- let base = std::sync::Arc::new(MemoryBlockStore::new());
147147+ let base = Arc::new(MemoryBlockStore::new());
143148 let writable = MemoryBlockStore::new();
144149145150 // Put data in base layer
···154159155160 #[tokio::test]
156161 async fn test_layered_writable_overrides_base() {
157157- let base = std::sync::Arc::new(MemoryBlockStore::new());
162162+ let base = Arc::new(MemoryBlockStore::new());
158163 let writable = MemoryBlockStore::new();
159164160165 // Put same content in both layers (will have same CID)
···183188184189 #[tokio::test]
185190 async fn test_layered_writes_to_writable_only() {
186186- let base = std::sync::Arc::new(MemoryBlockStore::new());
191191+ let base = Arc::new(MemoryBlockStore::new());
187192 let writable = MemoryBlockStore::new();
188193189194 let layered = LayeredBlockStore::new(writable.clone(), base.clone());
···200205201206 #[tokio::test]
202207 async fn test_layered_has_checks_both_layers() {
203203- let base = std::sync::Arc::new(MemoryBlockStore::new());
208208+ let base = Arc::new(MemoryBlockStore::new());
204209 let writable = MemoryBlockStore::new();
205210206211 let base_cid = base.put(b"base").await.unwrap();
+6-3
crates/jacquard-repo/src/storage/mod.rs
···11//! Block storage abstraction for MST nodes and records
2233+use crate::{error::Result, repo::CommitData};
34use bytes::Bytes;
45use cid::Cid as IpldCid;
55-use crate::error::Result;
6677/// Async block storage trait
88///
···6868 ///
6969 /// The provided CIDs should match the data, but implementations may choose to
7070 /// recalculate and validate them.
7171- async fn put_many(&self, blocks: impl IntoIterator<Item = (IpldCid, Bytes)> + Send) -> Result<()>;
7171+ async fn put_many(
7272+ &self,
7373+ blocks: impl IntoIterator<Item = (IpldCid, Bytes)> + Send,
7474+ ) -> Result<()>;
72757376 /// Get multiple blocks at once (optimization for batch reads)
7477 ///
···8790 /// This should be atomic where possible - either both operations succeed or both fail.
8891 /// For implementations that don't support atomic operations, writes should happen first,
8992 /// then deletes.
9090- async fn apply_commit(&self, commit: crate::repo::CommitData) -> Result<()>;
9393+ async fn apply_commit(&self, commit: CommitData) -> Result<()>;
9194}
92959396pub mod file;