atproto libraries implementation in ocaml
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 -> ())