1 import Base 2 import ./Reading.bend as Reading 3 import ./LogicalKey.bend as LogicalKey 4 import ./ContentHash.bend as ContentHash 5 import ./StateTree.bend as StateTree 6 import ./Staging.bend as Staging 7 import ./ContentObject.bend as ContentObject 8 import ./JsonAdapter.bend as JsonAdapter 9 import ./Value.bend as Value 10 import ./Store.bend as Store 11 import bend-kit-json@0.3.0.0/json.bend as Json 12 13 # Linear history: staging -> merged tree -> commit -> per-key version index. 14 # Commits serialize as canonical JSON without timestamp/certificate (both 15 # re-materialize on load: 0 / None). Loads re-serialize the parsed fields and 16 # compare against the stored text: a mismatch yields None, never corruption. 17 # Kit is named for the Val TYPE only; operations go through JsonAdapter. 18 # Effects are Store Op actions composed in do-blocks. Reads resolve to Values. 19 20 # One metadata key/value field. 21 type CommitMeta is Data: 22 Field{field_key: String, field_value: String} 23 24 # Identifier, parents, tree hash, timestamp, metadata and certificate. 25 type Commit is Data: 26 Make{commit_identifier: String, parent_commit_identifiers: List<&2, String>, tree_hash: String, timestamp: Nat, metadata: List<&2, CommitMeta>, certificate_hash: Maybe<&2, String>} 27 28 # Projects the identifier out of a commit. 29 def commit_identifier_of(commit_value: Commit) -> String: 30 match commit_value: 31 case Make{commit_identifier, parent_commit_identifiers, tree_hash, timestamp, metadata, certificate_hash}: 32 commit_identifier 33 34 # Projects the tree hash out of a commit. 35 def commit_tree_of(commit_value: Commit) -> String: 36 match commit_value: 37 case Make{commit_identifier, parent_commit_identifiers, tree_hash, timestamp, metadata, certificate_hash}: 38 tree_hash 39 40 # Encodes one metadata field as a two-item array. 41 def meta_pair_to_json(meta_field: CommitMeta) -> Json.Val: 42 match meta_field: 43 case Field{field_key, field_value}: 44 JsonAdapter.make_arr(Con{JsonAdapter.make_str(field_key), Con{JsonAdapter.make_str(field_value), Nil{}}}) 45 46 # Encodes the metadata list. 47 def meta_list_to_json(metadata: List<&2, CommitMeta>) -> List<&2, Json.Val>: 48 match metadata: 49 case Nil{}: 50 Nil{} 51 case Con{head_field, remaining_fields}: 52 Con{meta_pair_to_json(head_field), meta_list_to_json(remaining_fields)} 53 54 # Encodes parent identifiers as strings. 55 def encode_parent_list(parent_commit_identifiers: List<&2, String>) -> List<&2, Json.Val>: 56 match parent_commit_identifiers: 57 case Nil{}: 58 Nil{} 59 case Con{head_parent, remaining_parents}: 60 Con{JsonAdapter.make_str(head_parent), encode_parent_list(remaining_parents)} 61 62 # Encodes the parents/tree/meta document canonically. 63 def finalize_commit_document(parents_field: Sigma<&2, &2, String, _ => Json.Val>, tree_field: Sigma<&2, &2, String, _ => Json.Val>, meta_field: Sigma<&2, &2, String, _ => Json.Val>) -> String: 64 JsonAdapter.encode_canonical(JsonAdapter.make_obj(Con{parents_field, Con{tree_field, Con{meta_field, Nil{}}}})) 65 66 # Serializes parents, tree and metadata; the hashed bytes. 67 def serialize_commit(parent_commit_identifiers: List<&2, String>, metadata: List<&2, CommitMeta>, tree_hash_value: String) -> String: 68 finalize_commit_document(JsonAdapter.make_kv("parents", JsonAdapter.make_arr(encode_parent_list(parent_commit_identifiers))), JsonAdapter.make_kv("tree", JsonAdapter.make_str(tree_hash_value)), JsonAdapter.make_kv("meta", JsonAdapter.make_arr(meta_list_to_json(metadata)))) 69 70 # Conses a decoded meta field onto the tail. 71 def prepend_meta_field(field_key: String, field_value: String, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>: 72 match parsed_tail: 73 case None{}: 74 None{} 75 case Some{tail_fields}: 76 Some{Con{Field{field_key, field_value}, tail_fields}} 77 78 # Unwraps the field value, then prepends. 79 def prepend_meta_value(field_key: String, value_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>: 80 match value_maybe: 81 case None{}: 82 None{} 83 case Some{field_value}: 84 prepend_meta_field(field_key, field_value, parsed_tail) 85 86 # Unwraps the key text, then continues. 87 def prepend_meta_texts(key_maybe: Maybe<&2, String>, value_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>: 88 match key_maybe: 89 case None{}: 90 None{} 91 case Some{field_key}: 92 prepend_meta_value(field_key, value_maybe, parsed_tail) 93 94 # Decodes one [key, value] array; None on wrong shape. 95 def prepend_meta_items(pair_items: List<&2, Json.Val>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>: 96 match pair_items: 97 case Con{key_json, Con{value_json, Nil{}}}: 98 prepend_meta_texts(JsonAdapter.as_str(key_json), JsonAdapter.as_str(value_json), parsed_tail) 99 case _: 100 None{} 101 102 # Unwraps the pair array, then decodes. 103 def prepend_parsed_meta(pair_maybe: Maybe<&2, List<&2, Json.Val>>, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>: 104 match pair_maybe: 105 case None{}: 106 None{} 107 case Some{pair_items}: 108 prepend_meta_items(pair_items, parsed_tail) 109 110 # Decodes one metadata item. 111 def prepend_meta_entry(head_item: Json.Val, parsed_tail: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, List<&2, CommitMeta>>: 112 prepend_parsed_meta(JsonAdapter.as_arr(head_item), parsed_tail) 113 114 # Decodes the metadata array; None on any bad item. 115 def parse_meta_list(meta_items: List<&2, Json.Val>) -> Maybe<&2, List<&2, CommitMeta>>: 116 match meta_items: 117 case Nil{}: 118 Some{Nil{}} 119 case Con{head_item, remaining_items}: 120 prepend_meta_entry(head_item, parse_meta_list(remaining_items)) 121 122 # Accepts only when re-serialization matches stored bytes. 123 def verify_commit_text(text_matches: Bool, +requested_identifier: String, +parent_list: List<&2, String>, +tree_text: String, +parsed_meta: List<&2, CommitMeta>) -> Maybe<&2, Commit>: 124 match text_matches: 125 case True{}: 126 Some{Make{requested_identifier, parent_list, tree_text, 0n, parsed_meta, None{}}} 127 case False{}: 128 None{} 129 130 # Continues commit extraction once metadata decodes. 131 def finish_commit_meta(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, +tree_text: String, meta_maybe: Maybe<&2, List<&2, CommitMeta>>) -> Maybe<&2, Commit>: 132 match meta_maybe: 133 case None{}: 134 None{} 135 case Some{+parsed_meta}: 136 verify_commit_text(String.eq(serialize_commit(parent_list, parsed_meta, tree_text), stored_text), requested_identifier, parent_list, tree_text, parsed_meta) 137 138 # Continues commit extraction once the meta items decode. 139 def finish_commit(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, +tree_text: String, meta_items_maybe: Maybe<&2, List<&2, Json.Val>>) -> Maybe<&2, Commit>: 140 match meta_items_maybe: 141 case None{}: 142 None{} 143 case Some{meta_items}: 144 finish_commit_meta(requested_identifier, stored_text, parent_list, tree_text, parse_meta_list(meta_items)) 145 146 # Unwraps the meta array, then continues. 147 def finish_meta_array(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, +tree_text: String, meta_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, Commit>: 148 match meta_maybe: 149 case None{}: 150 None{} 151 case Some{meta_json}: 152 finish_commit(requested_identifier, stored_text, parent_list, tree_text, JsonAdapter.as_arr(meta_json)) 153 154 # Unwraps the tree field as text. 155 def tree_maybe_as_text(tree_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, String>: 156 match tree_maybe: 157 case None{}: 158 None{} 159 case Some{tree_json}: 160 JsonAdapter.as_str(tree_json) 161 162 # Continues commit extraction once the tree text decodes. 163 def extract_commit_meta(+requested_identifier: String, +stored_text: String, +parent_list: List<&2, String>, tree_text_maybe: Maybe<&2, String>, +document: Json.Val) -> Maybe<&2, Commit>: 164 match tree_text_maybe: 165 case None{}: 166 None{} 167 case Some{tree_text}: 168 finish_meta_array(requested_identifier, stored_text, parent_list, tree_text, JsonAdapter.get_field(document, "meta")) 169 170 # Continues commit extraction once parents decode. 171 def extract_commit_tree(+requested_identifier: String, +stored_text: String, parent_strings_maybe: Maybe<&2, List<&2, String>>, tree_maybe: Maybe<&2, Json.Val>, +document: Json.Val) -> Maybe<&2, Commit>: 172 match parent_strings_maybe: 173 case None{}: 174 None{} 175 case Some{parent_list}: 176 extract_commit_meta(requested_identifier, stored_text, parent_list, tree_maybe_as_text(tree_maybe), document) 177 178 # Unwraps the parent items, then continues. 179 def extract_parents_items(+requested_identifier: String, +stored_text: String, items_maybe: Maybe<&2, List<&2, Json.Val>>, +document: Json.Val) -> Maybe<&2, Commit>: 180 match items_maybe: 181 case None{}: 182 None{} 183 case Some{parent_items}: 184 extract_commit_tree(requested_identifier, stored_text, ContentObject.extract_each_string(parent_items), JsonAdapter.get_field(document, "tree"), document) 185 186 # Unwraps the parents field, then continues. 187 def extract_parents_array(+requested_identifier: String, +stored_text: String, parents_maybe: Maybe<&2, Json.Val>, +document: Json.Val) -> Maybe<&2, Commit>: 188 match parents_maybe: 189 case None{}: 190 None{} 191 case Some{parents_json}: 192 extract_parents_items(requested_identifier, stored_text, JsonAdapter.as_arr(parents_json), document) 193 194 # Reads the parents field of the document. 195 def extract_commit_parents(+requested_identifier: String, +stored_text: String, +document: Json.Val) -> Maybe<&2, Commit>: 196 extract_parents_array(requested_identifier, stored_text, JsonAdapter.get_field(document, "parents"), document) 197 198 # Builds the commit from parsed JSON; None on malformed input. 199 def commit_from_parse(requested_identifier: String, stored_text: String, parse_result: Maybe<&2, Json.Val>) -> Maybe<&2, Commit>: 200 match parse_result: 201 case None{}: 202 None{} 203 case Some{+document}: 204 extract_commit_parents(requested_identifier, stored_text, document) 205 206 # Parses stored commit text. 207 def parse_commit_text(requested_identifier: String, +stored_text: String) -> Maybe<&2, Commit>: 208 commit_from_parse(requested_identifier, stored_text, JsonAdapter.parse_text(stored_text)) 209 210 # Parses loaded text; missing storage reads as None. 211 def parse_commit(requested_identifier: String, stored_maybe: Maybe<&2, String>) -> Maybe<&2, Commit>: 212 match stored_maybe: 213 case None{}: 214 None{} 215 case Some{stored_text}: 216 parse_commit_text(requested_identifier, stored_text) 217 218 # Loads and verifies a commit by identifier. 219 def load_commit(+requested_identifier: String) -> Store.Op<&2, Maybe<&2, Commit>>: 220 do Store.Op<&2, Maybe<&2, Commit>>: 221 stored_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_commit_storage_key(requested_identifier)) 222 return parse_commit(requested_identifier, stored_maybe) 223 224 # Empty Sess result of unit type. 225 def empty_unit() -> Store.Op<&2, Unit>: 226 do Store.Op<&2, Unit>: 227 return Unit{} 228 229 # Empty Sess result of entry-list type. 230 def empty_pair_list() -> Store.Op<&2, List<&2, Reading.ChainEntry>>: 231 do Store.Op<&2, List<&2, Reading.ChainEntry>>: 232 return Nil{} 233 234 # Empty Sess result of tree type. 235 def empty_tree_maybe() -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 236 do Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 237 return None{} 238 239 # Serializes, hashes and stores a commit. 240 def persist_commit(+parent_commit_identifiers: List<&2, String>, +metadata: List<&2, CommitMeta>, +tree_hash_value: String) -> Store.Op<&2, Commit>: 241 do Store.Op<&2, Commit>: 242 +commit_text : String = serialize_commit(parent_commit_identifiers, metadata, tree_hash_value) 243 +commit_identifier : String = ContentHash.compute_sha256_hex(commit_text) 244 stored_unit : Unit <- Store.oput(LogicalKey.build_commit_storage_key(commit_identifier), commit_text) 245 return Make{commit_identifier, parent_commit_identifiers, tree_hash_value, 0n, metadata, None{}} 246 247 # Hashes and stores a merged entry list as a tree. 248 def persist_tree(+merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, String>: 249 do Store.Op<&2, String>: 250 +tree_hash_value : String = StateTree.compute_tree_hash(merged_entries) 251 stored_unit : Unit <- Store.oput(LogicalKey.build_tree_storage_key(tree_hash_value), StateTree.serialize_tree(merged_entries, tree_hash_value)) 252 return tree_hash_value 253 254 # Writes one version-index entry per merged entry. 255 def write_each_index(merged_entries: List<&2, Reading.ChainEntry>, +commit_value: Commit) -> Store.Op<&2, Unit>: 256 match merged_entries: 257 case Nil{}: 258 empty_unit() 259 case Con{+head_entry, remaining_entries}: 260 do Store.Op<&2, Unit>: 261 head_unit : Unit <- Store.oput(LogicalKey.build_version_index_key(Reading.entry_key_of(head_entry), commit_identifier_of(commit_value)), Reading.entry_value_of(head_entry)) 262 tail_unit : Unit <- write_each_index(remaining_entries, commit_value) 263 return tail_unit 264 265 # Removes staged keys consumed by the commit. 266 def clear_stage_keys(+session_identifier: String, merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, Unit>: 267 match merged_entries: 268 case Nil{}: 269 empty_unit() 270 case Con{head_entry, remaining_entries}: 271 do Store.Op<&2, Unit>: 272 cleared_unit : Unit <- Store.odel(LogicalKey.build_stage_storage_key_combined(session_identifier, Reading.entry_key_of(head_entry))) 273 cleared_tail : Unit <- clear_stage_keys(session_identifier, remaining_entries) 274 return cleared_tail 275 276 # Writes the index and clears the stage in sequence. 277 def persist_index_and_clear(+session_identifier: String, +merged_entries: List<&2, Reading.ChainEntry>, +commit_value: Commit) -> Store.Op<&2, Unit>: 278 do Store.Op<&2, Unit>: 279 indexed_unit : Unit <- write_each_index(merged_entries, commit_value) 280 cleared_unit : Unit <- clear_stage_keys(session_identifier, merged_entries) 281 index_cleared_unit : Unit <- Store.odel(Staging.stage_index_key(session_identifier)) 282 return index_cleared_unit 283 284 # Persists tree, commit and index, then clears the stage. 285 def materialize_commit(+session_identifier: String, +parent_commit_identifiers: List<&2, String>, metadata: List<&2, CommitMeta>, +merged_entries: List<&2, Reading.ChainEntry>) -> Store.Op<&2, Commit>: 286 do Store.Op<&2, Commit>: 287 +tree_hash_value : String <- persist_tree(merged_entries) 288 +commit_value : Commit <- persist_commit(parent_commit_identifiers, metadata, tree_hash_value) 289 cleared_unit : Unit <- persist_index_and_clear(session_identifier, merged_entries, commit_value) 290 return commit_value 291 292 # Parses loaded tree text. 293 def parse_tree_text(tree_hash_value: String, tree_maybe: Maybe<&2, String>) -> Maybe<&2, StateTree.StateTreeNode>: 294 match tree_maybe: 295 case None{}: 296 None{} 297 case Some{tree_text}: 298 StateTree.parse_tree_document(tree_text) 299 300 # Loads a tree by hash. 301 def load_tree_text(+tree_hash_value: String) -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 302 do Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 303 tree_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_tree_storage_key(tree_hash_value)) 304 return parse_tree_text(tree_hash_value, tree_maybe) 305 306 # Loads the tree a commit points at. 307 def load_tree_by_commit(commit_maybe: Maybe<&2, Commit>) -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 308 match commit_maybe: 309 case None{}: 310 empty_tree_maybe() 311 case Some{found_commit}: 312 load_tree_text(commit_tree_of(found_commit)) 313 314 # Reads the tree for a commit identifier. 315 def read_tree_at(+commit_identifier: String) -> Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 316 do Store.Op<&2, Maybe<&2, StateTree.StateTreeNode>>: 317 commit_maybe : Maybe<&2, Commit> <- load_commit(commit_identifier) 318 loaded_tree : Maybe<&2, StateTree.StateTreeNode> <- load_tree_by_commit(commit_maybe) 319 return loaded_tree 320 321 # Missing base trees read as empty. 322 def base_or_empty(tree_maybe: Maybe<&2, StateTree.StateTreeNode>) -> List<&2, Reading.ChainEntry>: 323 match tree_maybe: 324 case None{}: 325 Nil{} 326 case Some{parent_tree}: 327 StateTree.tree_entries(parent_tree) 328 329 # Loads the first parent entries. 330 def load_parent_tree(head_parent: String) -> Store.Op<&2, List<&2, Reading.ChainEntry>>: 331 do Store.Op<&2, List<&2, Reading.ChainEntry>>: 332 tree_maybe : Maybe<&2, StateTree.StateTreeNode> <- read_tree_at(head_parent) 333 return base_or_empty(tree_maybe) 334 335 # Loads base entries; root commits start empty. 336 def load_base_entries(parent_commit_identifiers: List<&2, String>) -> Store.Op<&2, List<&2, Reading.ChainEntry>>: 337 match parent_commit_identifiers: 338 case Nil{}: 339 empty_pair_list() 340 case Con{head_parent, remaining_parents}: 341 load_parent_tree(head_parent) 342 343 # Skips missing staged values, keeps present ones. 344 def prepend_pair_maybe(+head_key: String, head_value_maybe: Maybe<&2, String>, tail_pairs: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>: 345 match head_value_maybe: 346 case None{}: 347 tail_pairs 348 case Some{head_value}: 349 Con{Reading.make_entry(head_key, head_value), tail_pairs} 350 351 # Loads one staged value and prepends it. 352 def prepend_loaded_pair(+session_identifier: String, +head_key: String, tail_action: Store.Op<&2, List<&2, Reading.ChainEntry>>) -> Store.Op<&2, List<&2, Reading.ChainEntry>>: 353 do Store.Op<&2, List<&2, Reading.ChainEntry>>: 354 head_value_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_stage_storage_key_combined(session_identifier, head_key)) 355 tail_pairs : List<&2, Reading.ChainEntry> <- tail_action 356 return prepend_pair_maybe(head_key, head_value_maybe, tail_pairs) 357 358 # Loads every staged key in the manifest. 359 def load_each_pair(+session_identifier: String, staged_keys: List<&2, String>) -> Store.Op<&2, List<&2, Reading.ChainEntry>>: 360 match staged_keys: 361 case Nil{}: 362 empty_pair_list() 363 case Con{head_key, remaining_keys}: 364 prepend_loaded_pair(session_identifier, head_key, load_each_pair(session_identifier, remaining_keys)) 365 366 # Collects the session staged pairs. 367 def collect_staged_pairs(+session_identifier: String) -> Store.Op<&2, List<&2, Reading.ChainEntry>>: 368 do Store.Op<&2, List<&2, Reading.ChainEntry>>: 369 staged_keys : List<&2, String> <- Staging.load_stage_index(session_identifier) 370 collected_pairs : List<&2, Reading.ChainEntry> <- load_each_pair(session_identifier, staged_keys) 371 return collected_pairs 372 373 # Materializes stage plus base into a new commit. 374 def create_commit(+session_identifier: String, +parent_commit_identifiers: List<&2, String>, metadata: List<&2, CommitMeta>) -> Store.Op<&2, Commit>: 375 do Store.Op<&2, Commit>: 376 staged_pairs : List<&2, Reading.ChainEntry> <- collect_staged_pairs(session_identifier) 377 base_entries : List<&2, Reading.ChainEntry> <- load_base_entries(parent_commit_identifiers) 378 finished_commit : Commit <- materialize_commit(session_identifier, parent_commit_identifiers, metadata, StateTree.merge_sorted_entries(staged_pairs, base_entries)) 379 return finished_commit 380 381 # Versioned reads walk the first-parent chain with Nat fuel; unknown 382 # commits contribute "" links that never match a real index key. 383 def commit_parents_of(commit_value: Commit) -> List<&2, String>: 384 match commit_value: 385 case Make{commit_identifier, parent_commit_identifiers, tree_hash, timestamp, metadata, certificate_hash}: 386 parent_commit_identifiers 387 388 # Empty Sess result of string-list type. 389 def empty_string_list() -> Store.Op<&2, List<&2, String>>: 390 do Store.Op<&2, List<&2, String>>: 391 return Nil{} 392 393 # Empty Sess result of entry type. 394 def empty_entry_maybe() -> Store.Op<&2, Maybe<&2, Reading.ChainEntry>>: 395 do Store.Op<&2, Maybe<&2, Reading.ChainEntry>>: 396 return None{} 397 398 # Empty Op result of value type. 399 def empty_value_maybe() -> Store.Op<&2, Maybe<&2, Value.Value>>: 400 do Store.Op<&2, Maybe<&2, Value.Value>>: 401 return None{} 402 403 # Empty Op result of text type (commit identifiers, not values). 404 def empty_text_maybe() -> Store.Op<&2, Maybe<&2, String>>: 405 do Store.Op<&2, Maybe<&2, String>>: 406 return None{} 407 408 # Heads the parent list; root commits yield empty. 409 def first_parent_name(parent_identifiers: List<&2, String>) -> String: 410 match parent_identifiers: 411 case Nil{}: 412 "" 413 case Con{head_parent, remaining_parents}: 414 head_parent 415 416 # Heads the parent list; unknown commits yield empty. 417 def first_parent_text(commit_maybe: Maybe<&2, Commit>) -> String: 418 match commit_maybe: 419 case None{}: 420 "" 421 case Some{found_commit}: 422 first_parent_name(commit_parents_of(found_commit)) 423 424 # Unknown links end the walk: their index probes can never hit, so search 425 # results are unchanged while misses stop descending (empty parent text is 426 # terminal: missing commits and parentless roots both resolve to ""). 427 def fuel_for_empty(is_empty: Bool, gated_left: Nat) -> Nat: 428 match is_empty: 429 case True{}: 430 0n 431 case False{}: 432 gated_left 433 434 # Gates the remaining fuel on parent emptiness. 435 def fuel_for_parent(+parent_text: String, gated_left: Nat) -> Nat: 436 fuel_for_empty(String.eq(parent_text, ""), gated_left) 437 438 # Walks first-parents with fuel; newest first. checker_fuel decreases every 439 # level for the termination checker; gated_fuel carries the early stop. 440 # Callers pass the same fuel twice. 441 def walk_commit_chain(checker_fuel: Nat, gated_fuel: Nat, +commit_identifier: String) -> Store.Op<&2, List<&2, String>>: 442 match checker_fuel gated_fuel: 443 case 0n _: 444 empty_string_list() 445 case 1n+checker_left 0n: 446 empty_string_list() 447 case 1n+checker_left 1n+gated_left: 448 do Store.Op<&2, List<&2, String>>: 449 commit_maybe : Maybe<&2, Commit> <- load_commit(commit_identifier) 450 +parent_text : String = first_parent_text(commit_maybe) 451 tail_chain : List<&2, String> <- walk_commit_chain(checker_left, fuel_for_parent(parent_text, gated_left), parent_text) 452 return Con{commit_identifier, tail_chain} 453 454 # A stored value wins; otherwise the tail result. 455 def decide_search_hit(stored_maybe: Maybe<&2, String>, +lookup_key: String, tail_result: Maybe<&2, Reading.ChainEntry>) -> Maybe<&2, Reading.ChainEntry>: 456 match stored_maybe: 457 case None{}: 458 tail_result 459 case Some{stored_value}: 460 Some{Reading.make_entry(lookup_key, stored_value)} 461 462 # Searches the ancestor chain newest-first. 463 def search_chain_entries(ancestor_chain: List<&2, String>, +namespace_name: String, +record_identifier: String) -> Store.Op<&2, Maybe<&2, Reading.ChainEntry>>: 464 match ancestor_chain: 465 case Nil{}: 466 empty_entry_maybe() 467 case Con{head_commit, remaining_commits}: 468 do Store.Op<&2, Maybe<&2, Reading.ChainEntry>>: 469 +lookup_key : String = Reading.encode_lookup_key(namespace_name, record_identifier) 470 stored_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_version_index_key(lookup_key, head_commit)) 471 tail_result : Maybe<&2, Reading.ChainEntry> <- search_chain_entries(remaining_commits, namespace_name, record_identifier) 472 return decide_search_hit(stored_maybe, lookup_key, tail_result) 473 474 # Projects the payload out of a loaded object. 475 def payload_or_empty(loaded_maybe: Maybe<&2, ContentObject.ContentObject>) -> Maybe<&2, Value.Value>: 476 match loaded_maybe: 477 case None{}: 478 None{} 479 case Some{loaded_object}: 480 Some{ContentObject.object_payload_of(loaded_object)} 481 482 # Loads an object payload by content hash. 483 def load_object_payload(+value_hash: String) -> Store.Op<&2, Maybe<&2, Value.Value>>: 484 do Store.Op<&2, Maybe<&2, Value.Value>>: 485 loaded_maybe : Maybe<&2, ContentObject.ContentObject> <- ContentObject.load_content_object(value_hash) 486 return payload_or_empty(loaded_maybe) 487 488 # Tombstones read as absent; hashes load payloads. 489 def decide_resolve_value(is_tombstone: Bool, +stored_value: String) -> Store.Op<&2, Maybe<&2, Value.Value>>: 490 match is_tombstone: 491 case True{}: 492 empty_value_maybe() 493 case False{}: 494 load_object_payload(stored_value) 495 496 # Resolves one stored index value. 497 def resolve_entry_value(+stored_value: String) -> Store.Op<&2, Maybe<&2, Value.Value>>: 498 decide_resolve_value(String.eq(stored_value, "TOMBSTONE"), stored_value) 499 500 # Resolves a found chain entry to its payload. 501 def resolve_found_entry(found_maybe: Maybe<&2, Reading.ChainEntry>) -> Store.Op<&2, Maybe<&2, Value.Value>>: 502 match found_maybe: 503 case None{}: 504 empty_value_maybe() 505 case Some{found_entry}: 506 resolve_entry_value(Reading.entry_value_of(found_entry)) 507 508 # Reads a namespaced value at a commit. 509 def read_value_at(+namespace_name: String, +record_identifier: String, +commit_identifier: String) -> Store.Op<&2, Maybe<&2, Value.Value>>: 510 do Store.Op<&2, Maybe<&2, Value.Value>>: 511 ancestor_chain : List<&2, String> <- walk_commit_chain(10000n, 10000n, commit_identifier) 512 found_entry : Maybe<&2, Reading.ChainEntry> <- search_chain_entries(ancestor_chain, namespace_name, record_identifier) 513 resolved_value : Maybe<&2, Value.Value> <- resolve_found_entry(found_entry) 514 return resolved_value