crdt library in ocaml implementing json-joy
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