atproto libraries implementation in ocaml
at main 6.0 kB view raw
1(** Repository operations for AT Protocol. 2 3 A repository is a signed collection of records belonging to a single 4 account. Records are organized in an MST (Merkle Search Tree) structure 5 where: 6 - Keys are "collection/rkey" (e.g., "app.bsky.feed.post/3jui7kd2z2t2y") 7 - Values are CIDs pointing to the record data 8 9 The repository maintains: 10 - A blockstore for all content-addressed blocks 11 - An MST root pointing to the current record tree 12 - Commit history (optional) *) 13 14open Atproto_ipld 15open Atproto_mst 16 17type error = 18 [ `Record_not_found 19 | `Invalid_collection 20 | `Invalid_rkey 21 | `Mst_error of string 22 | `Commit_error of Commit.error ] 23 24let pp_error fmt = function 25 | `Record_not_found -> Format.fprintf fmt "record not found" 26 | `Invalid_collection -> Format.fprintf fmt "invalid collection NSID" 27 | `Invalid_rkey -> Format.fprintf fmt "invalid record key" 28 | `Mst_error msg -> Format.fprintf fmt "MST error: %s" msg 29 | `Commit_error e -> Format.fprintf fmt "commit error: %a" Commit.pp_error e 30 31let error_to_string e = Format.asprintf "%a" pp_error e 32 33module Mst = Make (Memory_blockstore) 34(** MST instantiated with memory blockstore *) 35 36type t = { 37 did : string; (** Repository DID *) 38 blockstore : Memory_blockstore.t; (** Block storage *) 39 mst_root : Cid.t; (** Current MST root *) 40 commit : Commit.t option; (** Latest commit *) 41} 42(** Repository state *) 43 44(** Create a new empty repository *) 45let create ~did : t = 46 let blockstore = Memory_blockstore.create () in 47 let mst_root = Mst.create_empty blockstore in 48 { did; blockstore; mst_root; commit = None } 49 50(** Load a repository from a commit *) 51let of_commit ~(blockstore : Memory_blockstore.t) (commit : Commit.t) : t = 52 { did = commit.did; blockstore; mst_root = commit.data; commit = Some commit } 53 54(** Get the repository DID *) 55let did repo = repo.did 56 57(** Get the current MST root CID *) 58let mst_root repo = repo.mst_root 59 60(** Get the latest commit *) 61let commit repo = repo.commit 62 63(** Build record key from collection and rkey *) 64let make_record_key ~collection ~rkey = collection ^ "/" ^ rkey 65 66(** Parse a record key into collection and rkey *) 67let parse_record_key key = 68 match String.index_opt key '/' with 69 | None -> None 70 | Some idx -> 71 let collection = String.sub key 0 idx in 72 let rkey = String.sub key (idx + 1) (String.length key - idx - 1) in 73 Some (collection, rkey) 74 75(** Get a record CID by collection and rkey *) 76let get_record repo ~collection ~rkey : Cid.t option = 77 let key = make_record_key ~collection ~rkey in 78 Mst.get repo.blockstore repo.mst_root key 79 80(** Get record data by collection and rkey *) 81let get_record_data repo ~collection ~rkey : Dag_cbor.value option = 82 match get_record repo ~collection ~rkey with 83 | None -> None 84 | Some cid -> ( 85 match Memory_blockstore.get repo.blockstore cid with 86 | None -> None 87 | Some data -> ( 88 match Dag_cbor.decode data with 89 | Ok value -> Some value 90 | Error _ -> None)) 91 92(** Check if a record exists *) 93let has_record repo ~collection ~rkey : bool = 94 Option.is_some (get_record repo ~collection ~rkey) 95 96(** Create or update a record. Returns the new repository state and the CID of 97 the stored record. *) 98let put_record repo ~collection ~rkey (value : Dag_cbor.value) : t * Cid.t = 99 (* Encode and store the record data *) 100 let data = Dag_cbor.encode value in 101 let record_cid = Cid.of_dag_cbor data in 102 Memory_blockstore.put repo.blockstore record_cid data; 103 (* Add to MST *) 104 let key = make_record_key ~collection ~rkey in 105 let new_root = Mst.add repo.blockstore repo.mst_root key record_cid in 106 ({ repo with mst_root = new_root; commit = None }, record_cid) 107 108(** Delete a record. Returns the new repository state. *) 109let delete_record repo ~collection ~rkey : t = 110 let key = make_record_key ~collection ~rkey in 111 let new_root = Mst.delete repo.blockstore repo.mst_root key in 112 { repo with mst_root = new_root; commit = None } 113 114(** List all records in a collection *) 115let list_collection repo ~collection : (string * Cid.t) list = 116 let prefix = collection ^ "/" in 117 let entries = Mst.to_list repo.blockstore repo.mst_root in 118 List.filter_map 119 (fun (key, cid) -> 120 if 121 String.length key > String.length prefix 122 && String.sub key 0 (String.length prefix) = prefix 123 then 124 let rkey = 125 String.sub key (String.length prefix) 126 (String.length key - String.length prefix) 127 in 128 Some (rkey, cid) 129 else None) 130 entries 131 132(** List all collections in the repository *) 133let list_collections repo : string list = 134 let entries = Mst.to_list repo.blockstore repo.mst_root in 135 let collections = 136 List.filter_map 137 (fun (key, _) -> 138 match parse_record_key key with 139 | Some (collection, _) -> Some collection 140 | None -> None) 141 entries 142 in 143 (* Remove duplicates *) 144 List.sort_uniq String.compare collections 145 146(** Count total records in the repository *) 147let record_count repo : int = Mst.length repo.blockstore repo.mst_root 148 149(** Create a signed commit for the current state. Returns the updated repository 150 with the new commit. *) 151let commit_repo repo ~rev ~(key : Atproto_crypto.K256.private_key) : t = 152 let prev = Option.map Commit.cid repo.commit in 153 let new_commit = 154 Commit.create ~did:repo.did ~data:repo.mst_root ~rev ?prev ~key () 155 in 156 (* Store the commit block *) 157 let commit_data = Commit.to_dag_cbor new_commit in 158 let commit_cid = Cid.of_dag_cbor commit_data in 159 Memory_blockstore.put repo.blockstore commit_cid commit_data; 160 { repo with commit = Some new_commit } 161 162(** Get all blocks in the repository *) 163let blocks repo : (Cid.t * string) list = 164 Memory_blockstore.blocks repo.blockstore 165 166(** Iterate over all records in the repository *) 167let iter_records repo ~f = 168 Mst.iter repo.blockstore repo.mst_root ~f:(fun key cid -> 169 match parse_record_key key with 170 | Some (collection, rkey) -> f ~collection ~rkey cid 171 | None -> ())