~/bend-docscommunity

src/StateTree.bend source

src/StateTree.bend on the hub · documented module

import Baseimport ./Reading.bend as Readingimport ./ContentHash.bend as ContentHashimport ./JsonAdapter.bend as JsonAdapterimport bend-kit-json@0.3.0.0/json.bend as Json# Flat sorted state tree (v0.1: no children partitioning). Entries sorted by# combined key ("namespace/record"); staged entries win ties. The tree hash# covers the flattened entries only, never the JSON text. This module owns# both tree directions (serialize + parse); History only calls them.# Kit is named for the Val TYPE only; operations go through JsonAdapter.# Tree hash with sorted entries; children reserved for partitioning.type StateTreeNode is Data:  Make{tree_hash: String, entries: List<&2, Reading.ChainEntry>, children_hashes: List<&2, String>}# Projects the entries out of a tree node.def tree_entries(tree_node: StateTreeNode) -> List<&2, Reading.ChainEntry>:  match tree_node:    case Make{tree_hash, entries, children_hashes}:      entries# Projects the hash out of a tree node.def tree_hash_of(tree_node: StateTreeNode) -> String:  match tree_node:    case Make{tree_hash, entries, children_hashes}:      tree_hash# Appends one key=value pair to the flattened text.def flatten_head(+head_entry: Reading.ChainEntry, flattened_tail: String) -> String:  Reading.entry_key_of(head_entry) ++ "=" ++ Reading.entry_value_of(head_entry) ++ ";" ++ flattened_tail# Flattens sorted entries into the hashed text.def flatten_entries(sorted_entries: List<&2, Reading.ChainEntry>) -> String:  match sorted_entries:    case Nil{}:      ""    case Con{head_entry, remaining_entries}:      flatten_head(head_entry, flatten_entries(remaining_entries))# Hashes the flattened entries; the tree address.def compute_tree_hash(sorted_entries: List<&2, Reading.ChainEntry>) -> String:  ContentHash.compute_sha256_hex(flatten_entries(sorted_entries))# Merges one step by key order; staged wins ties.def decide_ordering(ordering: Cmp, base_head: Reading.ChainEntry, base_tail: List<&2, Reading.ChainEntry>, staged_head: Reading.ChainEntry, inserted_tail: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>:  match ordering:    case LT{}:      Con{base_head, inserted_tail}    case GT{}:      Con{staged_head, Con{base_head, base_tail}}    case EQ{}:      Con{staged_head, base_tail}# Unpacks the comparison result into the ordering step.def decide_insert(cmp_result: (String & String) & Cmp, base_head: Reading.ChainEntry, base_tail: List<&2, Reading.ChainEntry>, staged_head: Reading.ChainEntry, inserted_tail: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>:  match cmp_result:    case ((returned_first, returned_second), ordering):      decide_ordering(ordering, base_head, base_tail, staged_head, inserted_tail)# Inserts one staged entry into sorted position.def insert_single_entry(base_entries: List<&2, Reading.ChainEntry>, +staged_head: Reading.ChainEntry) -> List<&2, Reading.ChainEntry>:  match base_entries:    case Nil{}:      Con{staged_head, Nil{}}    case Con{+base_head, +base_tail}:      decide_insert(String.cmp(Reading.entry_key_of(base_head), Reading.entry_key_of(staged_head)), base_head, base_tail, staged_head, insert_single_entry(base_tail, staged_head))# Folds staged entries into the base list, sorted.def merge_sorted_entries(staged_entries: List<&2, Reading.ChainEntry>, base_entries: List<&2, Reading.ChainEntry>) -> List<&2, Reading.ChainEntry>:  match staged_entries:    case Nil{}:      base_entries    case Con{staged_head, staged_tail}:      merge_sorted_entries(staged_tail, insert_single_entry(base_entries, staged_head))# Encodes one entry as a two-item JSON array.def entry_pair_to_json(+head_entry: Reading.ChainEntry) -> Json.Val:  JsonAdapter.make_arr(Con{JsonAdapter.make_str(Reading.entry_key_of(head_entry)), Con{JsonAdapter.make_str(Reading.entry_value_of(head_entry)), Nil{}}})# Encodes the entry list as a JSON array.def entry_list_to_json(sorted_entries: List<&2, Reading.ChainEntry>) -> List<&2, Json.Val>:  match sorted_entries:    case Nil{}:      Nil{}    case Con{head_entry, remaining_entries}:      Con{entry_pair_to_json(head_entry), entry_list_to_json(remaining_entries)}# Encodes the hash/entries/children document canonically.def finalize_tree_document(hash_field: Sigma<&2, &2, String, _ => Json.Val>, entries_field: Sigma<&2, &2, String, _ => Json.Val>, children_field: Sigma<&2, &2, String, _ => Json.Val>) -> String:  JsonAdapter.encode_canonical(JsonAdapter.make_obj(Con{hash_field, Con{entries_field, Con{children_field, Nil{}}}}))# Serializes a merged entry list with its hash.def serialize_tree(+merged_entries: List<&2, Reading.ChainEntry>, +tree_hash_value: String) -> String:  finalize_tree_document(JsonAdapter.make_kv("hash", JsonAdapter.make_str(tree_hash_value)), JsonAdapter.make_kv("entries", JsonAdapter.make_arr(entry_list_to_json(merged_entries))), JsonAdapter.make_kv("children", JsonAdapter.make_arr(Nil{})))# Conses a decoded pair onto the tail.def prepend_entry_pair(key_text: String, value_text: String, parsed_tail: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  match parsed_tail:    case None{}:      None{}    case Some{tail_entries}:      Some{Con{Reading.make_entry(key_text, value_text), tail_entries}}# Unwraps the value text, then prepends.def prepend_value_text(key_text: String, value_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  match value_maybe:    case None{}:      None{}    case Some{value_text}:      prepend_entry_pair(key_text, value_text, parsed_tail)# Unwraps the key text, then continues.def prepend_pair_texts(key_maybe: Maybe<&2, String>, value_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  match key_maybe:    case None{}:      None{}    case Some{key_text}:      prepend_value_text(key_text, value_maybe, parsed_tail)# Decodes one [key, value] array; None on wrong shape.def prepend_pair_items(pair_items: List<&2, Json.Val>, parsed_tail: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  match pair_items:    case Con{key_json, Con{value_json, Nil{}}}:      prepend_pair_texts(JsonAdapter.as_str(key_json), JsonAdapter.as_str(value_json), parsed_tail)    case _:      None{}# Unwraps the pair array, then decodes.def prepend_parsed_pair(pair_maybe: Maybe<&2, List<&2, Json.Val>>, parsed_tail: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  match pair_maybe:    case None{}:      None{}    case Some{pair_items}:      prepend_pair_items(pair_items, parsed_tail)# Decodes one entry item.def prepend_parsed_entry(head_item: Json.Val, parsed_tail: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  prepend_parsed_pair(JsonAdapter.as_arr(head_item), parsed_tail)# Decodes the entry array; None on any bad item.def parse_entry_list(entry_items: List<&2, Json.Val>) -> Maybe<&2, List<&2, Reading.ChainEntry>>:  match entry_items:    case Nil{}:      Some{Nil{}}    case Con{head_item, remaining_items}:      prepend_parsed_entry(head_item, parse_entry_list(remaining_items))# Conses a decoded hash onto the tail.def prepend_child_tail(head_text: String, parsed_tail: Maybe<&2, List<&2, String>>) -> Maybe<&2, List<&2, String>>:  match parsed_tail:    case None{}:      None{}    case Some{tail_texts}:      Some{Con{head_text, tail_texts}}# Unwraps the child hash text, then prepends.def prepend_child_text(head_maybe: Maybe<&2, String>, parsed_tail: Maybe<&2, List<&2, String>>) -> Maybe<&2, List<&2, String>>:  match head_maybe:    case None{}:      None{}    case Some{head_text}:      prepend_child_tail(head_text, parsed_tail)# Decodes one child-hash item.def prepend_child_string(head_item: Json.Val, parsed_tail: Maybe<&2, List<&2, String>>) -> Maybe<&2, List<&2, String>>:  prepend_child_text(JsonAdapter.as_str(head_item), parsed_tail)# Decodes the children array; None on any non-string.def extract_child_strings(child_items: List<&2, Json.Val>) -> Maybe<&2, List<&2, String>>:  match child_items:    case Nil{}:      Some{Nil{}}    case Con{head_item, remaining_items}:      prepend_child_string(head_item, extract_child_strings(remaining_items))# Reassembles the node once children decode.def finish_tree_node(tree_hash_text: String, parsed_entries: List<&2, Reading.ChainEntry>, strings_maybe: Maybe<&2, List<&2, String>>) -> Maybe<&2, StateTreeNode>:  match strings_maybe:    case None{}:      None{}    case Some{child_hashes}:      Some{Make{tree_hash_text, parsed_entries, child_hashes}}# Unwraps the children array items, then finishes.def finish_children_strings(tree_hash_text: String, parsed_entries: List<&2, Reading.ChainEntry>, items_maybe: Maybe<&2, List<&2, Json.Val>>) -> Maybe<&2, StateTreeNode>:  match items_maybe:    case None{}:      None{}    case Some{child_items}:      finish_tree_node(tree_hash_text, parsed_entries, extract_child_strings(child_items))# Unwraps the children field, then continues.def extract_children_array(tree_hash_text: String, parsed_entries: List<&2, Reading.ChainEntry>, children_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, StateTreeNode>:  match children_maybe:    case None{}:      None{}    case Some{children_json}:      finish_children_strings(tree_hash_text, parsed_entries, JsonAdapter.as_arr(children_json))# Unwraps parsed entries, then reads children.def extract_children(+document: Json.Val, tree_hash_text: String, entries_maybe: Maybe<&2, List<&2, Reading.ChainEntry>>) -> Maybe<&2, StateTreeNode>:  match entries_maybe:    case None{}:      None{}    case Some{parsed_entries}:      extract_children_array(tree_hash_text, parsed_entries, JsonAdapter.get_field(document, "children"))# Unwraps the entry items, then parses.def extract_entry_items(+document: Json.Val, tree_hash_text: String, items_maybe: Maybe<&2, List<&2, Json.Val>>) -> Maybe<&2, StateTreeNode>:  match items_maybe:    case None{}:      None{}    case Some{entry_items}:      extract_children(document, tree_hash_text, parse_entry_list(entry_items))# Unwraps the entries field, then continues.def extract_entry_array(+document: Json.Val, tree_hash_text: String, entries_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, StateTreeNode>:  match entries_maybe:    case None{}:      None{}    case Some{entries_json}:      extract_entry_items(document, tree_hash_text, JsonAdapter.as_arr(entries_json))# Unwraps the hash text, then reads entries.def extract_with_tree_entries(+document: Json.Val, hash_text_maybe: Maybe<&2, String>, entries_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, StateTreeNode>:  match hash_text_maybe:    case None{}:      None{}    case Some{tree_hash_text}:      extract_entry_array(document, tree_hash_text, entries_maybe)# Unwraps the hash field, then continues.def extract_with_tree_hash(+document: Json.Val, hash_maybe: Maybe<&2, Json.Val>) -> Maybe<&2, StateTreeNode>:  match hash_maybe:    case None{}:      None{}    case Some{hash_json}:      extract_with_tree_entries(document, JsonAdapter.as_str(hash_json), JsonAdapter.get_field(document, "entries"))# Reads the hash field of the document.def extract_tree_fields(+document: Json.Val) -> Maybe<&2, StateTreeNode>:  extract_with_tree_hash(document, JsonAdapter.get_field(document, "hash"))# Builds the node from parsed JSON; None on malformed input.def tree_from_parse(parse_result: Maybe<&2, Json.Val>) -> Maybe<&2, StateTreeNode>:  match parse_result:    case None{}:      None{}    case Some{+document}:      extract_tree_fields(document)# Parses stored tree text into a node.def parse_tree_document(stored_text: String) -> Maybe<&2, StateTreeNode>:  tree_from_parse(JsonAdapter.parse_text(stored_text))