crdt library in ocaml implementing json-joy
at main 18 kB view raw
1(** High-level editing API. 2 3 Provides a user-friendly interface for editing CRDT documents with 4 path-based access, transaction support, and automatic node creation. 5 6 Key features: 7 - Path-based navigation using JSON Pointer syntax 8 - Transaction support with flush/commit 9 - Automatic node creation for nested structures 10 - Type-safe node proxies for editing 11 12 Usage: 13 {[ 14 let api = Model_api.create model in 15 16 (* Direct editing *) 17 Model_api.set api "/" (Value.String "hello"); 18 19 (* String editing *) 20 let str_node = Model_api.str api "/text" in 21 Model_api.str_insert str_node ~pos:0 ~text:"hello"; 22 23 (* Object editing *) 24 let obj_node = Model_api.obj api "/data" in 25 Model_api.obj_set obj_node ~key:"name" ~value:(Value.String "Alice"); 26 27 (* Flush changes to patch *) 28 let patch = Model_api.flush api 29 ]} *) 30 31(** {1 Types} *) 32 33type t = { model : Model.t; mutable builder : Patch_builder.t } 34(** An API handle for editing a model with transaction support *) 35 36(** {1 Creation} *) 37 38(** Create an API handle for a model *) 39let create model = { model; builder = Patch_builder.create model.Model.clock } 40 41(** Reset the transaction state, discarding any pending changes *) 42let reset api = Patch_builder.reset api.builder 43 44(** {1 Transaction Support} *) 45 46(** Flush pending operations as a patch. Returns None if no operations. *) 47let flush api = Patch_builder.flush api.builder 48 49(** Flush and apply the patch to the model atomically. Returns the patch. *) 50let commit api = 51 match flush api with 52 | Some patch -> 53 Model.apply api.model patch; 54 Some patch 55 | None -> None 56 57(** Check if there are pending operations *) 58let has_pending api = Patch_builder.has_pending api.builder 59 60(** Get the number of pending operations *) 61let pending_count api = Patch_builder.pending_count api.builder 62 63(** {1 Clock Access} *) 64 65(** Get the current logical time *) 66let current_time api = Patch_builder.current_time api.builder 67 68(** Get the session ID *) 69let session_id api = Patch_builder.session_id api.builder 70 71(** Get the model *) 72let model api = api.model 73 74(** {1 Path Resolution} *) 75 76(** Result of resolving a path *) 77type path_result = 78 | Found of Node.t (** Found the node *) 79 | Not_found (** Path doesn't exist *) 80 | Type_mismatch of string (** Wrong node type for path segment *) 81 82(** Resolve a JSON Pointer path to a node *) 83let resolve_path api (path : string) : path_result = 84 let pointer = Pointer.parse path in 85 let segments = Pointer.segments pointer in 86 (* Filter out empty segments (e.g., "/" parses as [""] which should be []) *) 87 let segments = List.filter (fun s -> s <> "") segments in 88 89 (* Start from root *) 90 let rec walk current_node remaining = 91 match remaining with 92 | [] -> Found current_node 93 | seg :: rest -> ( 94 match current_node with 95 | Node.Node_val { val_ref = Some ts; _ } -> ( 96 (* Follow the reference *) 97 match Model.get_node api.model ts with 98 | Some next -> walk next (seg :: rest) 99 | None -> Not_found) 100 | Node.Node_val { val_ref = None; _ } -> Not_found 101 | Node.Node_obj { obj_entries; _ } -> ( 102 (* Look up key in object *) 103 match 104 List.find_opt 105 (fun (e : Node.obj_entry) -> e.obj_key = seg) 106 obj_entries 107 with 108 | Some entry -> ( 109 match Model.get_node api.model entry.obj_value with 110 | Some next -> walk next rest 111 | None -> Not_found) 112 | None -> Not_found) 113 | Node.Node_vec { vec_slots; _ } -> ( 114 (* Parse index and look up slot *) 115 match int_of_string_opt seg with 116 | Some idx -> ( 117 match 118 List.find_opt 119 (fun (s : Node.vec_slot) -> s.vec_idx = idx) 120 vec_slots 121 with 122 | Some slot -> ( 123 match Model.get_node api.model slot.vec_value with 124 | Some next -> walk next rest 125 | None -> Not_found) 126 | None -> Not_found) 127 | None -> Type_mismatch "vec requires numeric index") 128 | Node.Node_arr _ -> Type_mismatch "arr traversal not supported" 129 | Node.Node_str _ -> Type_mismatch "str has no children" 130 | Node.Node_bin _ -> Type_mismatch "bin has no children" 131 | Node.Node_con _ -> Type_mismatch "con has no children") 132 in 133 walk api.model.root segments 134 135(** {1 Node Access} *) 136 137(** Get a node at a path, or None if not found *) 138let get api path = 139 match resolve_path api path with Found node -> Some node | _ -> None 140 141(** Get the value at a path *) 142let get_value api path = 143 match get api path with Some node -> Some (Node.view node) | None -> None 144 145(** Get the fully resolved value at a path *) 146let view api path = 147 match resolve_path api path with 148 | Found node -> Some (Model.view_node api.model node) 149 | _ -> None 150 151(** {1 Internal Helpers} *) 152 153(** Get the next ID that will be assigned (before adding an op) *) 154let next_id api : Clock.timestamp = Patch_builder.next_op_id api.builder 155 156(** Add a new_con operation and return the ID that will be assigned *) 157let add_const api value = 158 let id = next_id api in 159 Patch_builder.new_con api.builder value; 160 id 161 162(** Set the document root to a value *) 163let set_root api value = 164 let const_id = add_const api value in 165 Patch_builder.ins_val api.builder ~obj:Model.root_id ~value:const_id 166 167(** {1 Object Operations} *) 168 169type obj_proxy = { obj_api : t; obj_id : Clock.timestamp } 170(** Object proxy for editing *) 171 172(** Get or create an object at a path. For an existing object, returns a proxy 173 to edit it. For an unset val node, creates a new object and returns its 174 proxy. The object won't exist in the model until commit is called. *) 175let obj api path : obj_proxy option = 176 match resolve_path api path with 177 | Found (Node.Node_obj { obj_id; _ }) -> Some { obj_api = api; obj_id } 178 | Found (Node.Node_val { val_ref = None; val_id; _ }) -> 179 (* Create new object and set the val to point to it *) 180 let id = next_id api in 181 Patch_builder.new_obj api.builder; 182 Patch_builder.ins_val api.builder ~obj:val_id ~value:id; 183 Some { obj_api = api; obj_id = id } 184 | _ -> None 185 186(** Set a key in an object proxy. Returns the timestamp of the new value node. 187*) 188let obj_set proxy ~key ~value = 189 (* Create a const node for the value *) 190 let const_id = add_const proxy.obj_api value in 191 (* Insert into object *) 192 Patch_builder.ins_obj proxy.obj_api.builder ~obj:proxy.obj_id 193 ~entries:[ (key, const_id) ]; 194 const_id 195 196(** Delete a key from an object. This sets the key to undefined. *) 197let obj_delete proxy ~key = 198 let undef_id = add_const proxy.obj_api Value.Undefined in 199 Patch_builder.ins_obj proxy.obj_api.builder ~obj:proxy.obj_id 200 ~entries:[ (key, undef_id) ] 201 202(** Get a value from the object (only works after commit) *) 203let obj_get proxy ~key = 204 match Model.get_node proxy.obj_api.model proxy.obj_id with 205 | Some (Node.Node_obj { obj_entries; _ }) -> ( 206 match 207 List.find_opt (fun (e : Node.obj_entry) -> e.obj_key = key) obj_entries 208 with 209 | Some entry -> Model.get_node proxy.obj_api.model entry.obj_value 210 | None -> None) 211 | _ -> None 212 213(** Get all keys in the object (only works after commit) *) 214let obj_keys proxy = 215 match Model.get_node proxy.obj_api.model proxy.obj_id with 216 | Some (Node.Node_obj { obj_entries; _ }) -> 217 List.map (fun (e : Node.obj_entry) -> e.obj_key) obj_entries 218 | _ -> [] 219 220(** {1 String Operations} *) 221 222type str_proxy = { 223 str_api : t; 224 str_id : Clock.timestamp; 225 mutable last_insert_end : Clock.timestamp option; 226 (** End of last inserted text *) 227} 228(** String proxy for editing. Tracks the last insert position for sequential 229 appends. *) 230 231(** Get or create a string at a path *) 232let str api path : str_proxy option = 233 match resolve_path api path with 234 | Found (Node.Node_str { str_id; _ }) -> 235 Some { str_api = api; str_id; last_insert_end = None } 236 | Found (Node.Node_val { val_ref = None; val_id; _ }) -> 237 (* Create new string and set the val to point to it *) 238 let id = next_id api in 239 Patch_builder.new_str api.builder; 240 Patch_builder.ins_val api.builder ~obj:val_id ~value:id; 241 Some { str_api = api; str_id = id; last_insert_end = None } 242 | _ -> None 243 244(** Insert text at a position in a string. 245 @param pos Character position (0 = start) 246 @param text Text to insert *) 247let str_insert proxy ~pos ~text = 248 let insert_id = next_id proxy.str_api in 249 let text_len = String.length text in 250 match Model.get_node proxy.str_api.model proxy.str_id with 251 | Some (Node.Node_str { str_rga; _ }) -> 252 let after = 253 match Rga.find_position_string str_rga pos with 254 | Some ts -> ts 255 | None -> proxy.str_id (* Insert at head = after node ID *) 256 in 257 Patch_builder.ins_str proxy.str_api.builder ~obj:proxy.str_id ~after 258 ~value:text; 259 (* Track end of this insert *) 260 proxy.last_insert_end <- 261 Some { insert_id with time = insert_id.time + text_len - 1 } 262 | _ -> 263 (* Node doesn't exist yet - insert at head *) 264 Patch_builder.ins_str proxy.str_api.builder ~obj:proxy.str_id 265 ~after:proxy.str_id ~value:text; 266 proxy.last_insert_end <- 267 Some { insert_id with time = insert_id.time + text_len - 1 } 268 269(** Append text to a string *) 270let str_append proxy ~text = 271 let insert_id = next_id proxy.str_api in 272 let text_len = String.length text in 273 (* First check if we have a pending insert to chain after *) 274 let after = 275 match proxy.last_insert_end with 276 | Some end_id -> end_id (* Chain after previous insert *) 277 | None -> ( 278 (* No pending insert - check the actual RGA *) 279 match Model.get_node proxy.str_api.model proxy.str_id with 280 | Some (Node.Node_str { str_rga; _ }) -> ( 281 match Rga.find_last_id str_rga with 282 | Some ts -> ts 283 | None -> proxy.str_id) 284 | _ -> proxy.str_id (* Node doesn't exist yet *)) 285 in 286 Patch_builder.ins_str proxy.str_api.builder ~obj:proxy.str_id ~after 287 ~value:text; 288 proxy.last_insert_end <- 289 Some { insert_id with time = insert_id.time + text_len - 1 } 290 291(** Delete text from a string. 292 @param pos Start position 293 @param len Number of characters to delete *) 294let str_delete proxy ~pos ~len = 295 match Model.get_node proxy.str_api.model proxy.str_id with 296 | Some (Node.Node_str { str_rga; _ }) -> 297 let spans = Rga.find_spans_string str_rga ~pos ~len in 298 if spans <> [] then 299 Patch_builder.del proxy.str_api.builder ~obj:proxy.str_id ~what:spans 300 | _ -> () 301 302(** Get the current string value (only works after commit or for existing 303 strings) *) 304let str_value proxy = 305 match Model.get_node proxy.str_api.model proxy.str_id with 306 | Some (Node.Node_str { str_rga; _ }) -> Rga.view_string str_rga 307 | _ -> "" 308 309(** Get the string length *) 310let str_length proxy = 311 match Model.get_node proxy.str_api.model proxy.str_id with 312 | Some (Node.Node_str { str_rga; _ }) -> Rga.visible_length str_rga 313 | _ -> 0 314 315(** {1 Array Operations} *) 316 317type arr_proxy = { arr_api : t; arr_id : Clock.timestamp } 318(** Array proxy for editing *) 319 320(** Get or create an array at a path *) 321let arr api path : arr_proxy option = 322 match resolve_path api path with 323 | Found (Node.Node_arr { arr_id; _ }) -> Some { arr_api = api; arr_id } 324 | Found (Node.Node_val { val_ref = None; val_id; _ }) -> 325 let id = next_id api in 326 Patch_builder.new_arr api.builder; 327 Patch_builder.ins_val api.builder ~obj:val_id ~value:id; 328 Some { arr_api = api; arr_id = id } 329 | _ -> None 330 331(** Insert an element at a position in an array. 332 @param pos Array index (0 = start) 333 @param value Value to insert *) 334let arr_insert proxy ~pos ~value = 335 let const_id = add_const proxy.arr_api value in 336 match Model.get_node proxy.arr_api.model proxy.arr_id with 337 | Some (Node.Node_arr { arr_rga; _ }) -> 338 let after = 339 match Rga.find_position_arr arr_rga pos with 340 | Some ts -> ts 341 | None -> proxy.arr_id (* Insert at head *) 342 in 343 Patch_builder.ins_arr proxy.arr_api.builder ~obj:proxy.arr_id ~after 344 ~value:const_id 345 | _ -> 346 Patch_builder.ins_arr proxy.arr_api.builder ~obj:proxy.arr_id 347 ~after:proxy.arr_id ~value:const_id 348 349(** Append an element to an array *) 350let arr_push proxy ~value = 351 let const_id = add_const proxy.arr_api value in 352 match Model.get_node proxy.arr_api.model proxy.arr_id with 353 | Some (Node.Node_arr { arr_rga; _ }) -> 354 let after = 355 match Rga.find_last_id arr_rga with 356 | Some ts -> ts 357 | None -> proxy.arr_id (* Empty = insert at head *) 358 in 359 Patch_builder.ins_arr proxy.arr_api.builder ~obj:proxy.arr_id ~after 360 ~value:const_id 361 | _ -> 362 Patch_builder.ins_arr proxy.arr_api.builder ~obj:proxy.arr_id 363 ~after:proxy.arr_id ~value:const_id 364 365(** Delete elements from an array. 366 @param pos Start position 367 @param count Number of elements to delete *) 368let arr_delete proxy ~pos ~count = 369 match Model.get_node proxy.arr_api.model proxy.arr_id with 370 | Some (Node.Node_arr { arr_rga; _ }) -> 371 let spans = Rga.find_spans_arr arr_rga ~pos ~len:count in 372 if spans <> [] then 373 Patch_builder.del proxy.arr_api.builder ~obj:proxy.arr_id ~what:spans 374 | _ -> () 375 376(** Get the current array length *) 377let arr_length proxy = 378 match Model.get_node proxy.arr_api.model proxy.arr_id with 379 | Some (Node.Node_arr { arr_rga; _ }) -> Rga.visible_length arr_rga 380 | _ -> 0 381 382(** {1 Binary Operations} *) 383 384type bin_proxy = { bin_api : t; bin_id : Clock.timestamp } 385(** Binary proxy for editing *) 386 387(** Get or create a binary blob at a path *) 388let bin api path : bin_proxy option = 389 match resolve_path api path with 390 | Found (Node.Node_bin { bin_id; _ }) -> Some { bin_api = api; bin_id } 391 | Found (Node.Node_val { val_ref = None; val_id; _ }) -> 392 let id = next_id api in 393 Patch_builder.new_bin api.builder; 394 Patch_builder.ins_val api.builder ~obj:val_id ~value:id; 395 Some { bin_api = api; bin_id = id } 396 | _ -> None 397 398(** Insert bytes at a position *) 399let bin_insert proxy ~pos ~data = 400 match Model.get_node proxy.bin_api.model proxy.bin_id with 401 | Some (Node.Node_bin { bin_rga; _ }) -> 402 let after = 403 match Rga.find_position_bytes bin_rga pos with 404 | Some ts -> ts 405 | None -> proxy.bin_id (* Insert at head *) 406 in 407 Patch_builder.ins_bin proxy.bin_api.builder ~obj:proxy.bin_id ~after 408 ~value:data 409 | _ -> 410 Patch_builder.ins_bin proxy.bin_api.builder ~obj:proxy.bin_id 411 ~after:proxy.bin_id ~value:data 412 413(** Append bytes *) 414let bin_append proxy ~data = 415 match Model.get_node proxy.bin_api.model proxy.bin_id with 416 | Some (Node.Node_bin { bin_rga; _ }) -> 417 let after = 418 match Rga.find_last_id bin_rga with 419 | Some ts -> ts 420 | None -> proxy.bin_id (* Empty = insert at head *) 421 in 422 Patch_builder.ins_bin proxy.bin_api.builder ~obj:proxy.bin_id ~after 423 ~value:data 424 | _ -> 425 Patch_builder.ins_bin proxy.bin_api.builder ~obj:proxy.bin_id 426 ~after:proxy.bin_id ~value:data 427 428(** Delete bytes from binary. 429 @param pos Start position 430 @param len Number of bytes to delete *) 431let bin_delete proxy ~pos ~len = 432 match Model.get_node proxy.bin_api.model proxy.bin_id with 433 | Some (Node.Node_bin { bin_rga; _ }) -> 434 let spans = Rga.find_spans_bytes bin_rga ~pos ~len in 435 if spans <> [] then 436 Patch_builder.del proxy.bin_api.builder ~obj:proxy.bin_id ~what:spans 437 | _ -> () 438 439(** Get the current binary value *) 440let bin_value proxy = 441 match Model.get_node proxy.bin_api.model proxy.bin_id with 442 | Some (Node.Node_bin { bin_rga; _ }) -> Rga.view_bytes bin_rga 443 | _ -> Bytes.empty 444 445(** Get the binary length *) 446let bin_length proxy = 447 match Model.get_node proxy.bin_api.model proxy.bin_id with 448 | Some (Node.Node_bin { bin_rga; _ }) -> Rga.visible_length bin_rga 449 | _ -> 0 450 451(** {1 Vector Operations} *) 452 453type vec_proxy = { vec_api : t; vec_id : Clock.timestamp } 454(** Vector proxy for editing *) 455 456(** Get or create a vector at a path *) 457let vec api path : vec_proxy option = 458 match resolve_path api path with 459 | Found (Node.Node_vec { vec_id; _ }) -> Some { vec_api = api; vec_id } 460 | Found (Node.Node_val { val_ref = None; val_id; _ }) -> 461 let id = next_id api in 462 Patch_builder.new_vec api.builder; 463 Patch_builder.ins_val api.builder ~obj:val_id ~value:id; 464 Some { vec_api = api; vec_id = id } 465 | _ -> None 466 467(** Set a slot in a vector (0-255) *) 468let vec_set proxy ~idx ~value = 469 if idx < 0 || idx > 255 then invalid_arg "vec_set: idx out of range"; 470 let const_id = add_const proxy.vec_api value in 471 Patch_builder.ins_vec proxy.vec_api.builder ~obj:proxy.vec_id ~idx 472 ~value:const_id 473 474(** Get a slot value from the vector *) 475let vec_get proxy ~idx = 476 match Model.get_node proxy.vec_api.model proxy.vec_id with 477 | Some (Node.Node_vec { vec_slots; _ }) -> ( 478 match 479 List.find_opt (fun (s : Node.vec_slot) -> s.vec_idx = idx) vec_slots 480 with 481 | Some slot -> Model.get_node proxy.vec_api.model slot.vec_value 482 | None -> None) 483 | _ -> None 484 485(** {1 Document View} *) 486 487(** Get the complete document view *) 488let view_document api = Model.view api.model 489 490(** Pretty print the document *) 491let pp_document fmt api = 492 let value = Model.view api.model in 493 Value.pp fmt value