1 import Base 2 import ./LogicalKey.bend as LogicalKey 3 import ./ContentObject.bend as ContentObject 4 import ./JsonAdapter.bend as JsonAdapter 5 import ./Value.bend as Value 6 import ./Store.bend as Store 7 import bend-kit-json@0.3.0.0/json.bend as Json 8 9 # Uncommitted working set. Staged values live at stage/{session}/{ns}/{id}; 10 # the per-session key manifest lives at stage-index/{session} as a canonical 11 # JSON string list. Point operations only — no prefix scan anywhere. 12 # Kit is named for the Val TYPE only; operations go through JsonAdapter. 13 # Effects are Store Op actions composed in do-blocks. 14 15 # Addresses the per-session staged-key manifest. 16 def stage_index_key(+session_identifier: String) -> String: 17 "stage-index/" ++ session_identifier 18 19 # Encodes the manifest as a canonical JSON string list. 20 def serialize_key_list(staged_keys: List<&2, String>) -> String: 21 JsonAdapter.encode_canonical(JsonAdapter.make_arr(ContentObject.encode_each_reference(staged_keys))) 22 23 # Treats a failed decode as an empty manifest. 24 def index_from_strings(strings_maybe: Maybe<&2, List<&2, String>>) -> List<&2, String>: 25 match strings_maybe: 26 case None{}: 27 Nil{} 28 case Some{key_texts}: 29 key_texts 30 31 # Unwraps the manifest array, then decodes. 32 def index_from_document(items_maybe: Maybe<&2, List<&2, Json.Val>>) -> List<&2, String>: 33 match items_maybe: 34 case None{}: 35 Nil{} 36 case Some{json_items}: 37 index_from_strings(ContentObject.extract_each_string(json_items)) 38 39 # Parses stored manifest text; malformed reads as empty. 40 def index_from_text(parse_result: Maybe<&2, Json.Val>) -> List<&2, String>: 41 match parse_result: 42 case None{}: 43 Nil{} 44 case Some{document}: 45 index_from_document(JsonAdapter.as_arr(document)) 46 47 # Loads the manifest; missing storage reads as empty. 48 def parse_stage_index(stored_maybe: Maybe<&2, String>) -> List<&2, String>: 49 match stored_maybe: 50 case None{}: 51 Nil{} 52 case Some{stored_text}: 53 index_from_text(JsonAdapter.parse_text(stored_text)) 54 55 # True short-circuits the membership scan. 56 def decide_contains(head_matches: Bool, recursive_result: Bool) -> Bool: 57 match head_matches: 58 case True{}: 59 True{} 60 case False{}: 61 recursive_result 62 63 # Scans the manifest for the target key. 64 def contains_staged_key(+target_key: String, staged_keys: List<&2, String>) -> Bool: 65 match staged_keys: 66 case Nil{}: 67 False{} 68 case Con{head_key, remaining_keys}: 69 decide_contains(String.eq(head_key, target_key), contains_staged_key(target_key, remaining_keys)) 70 71 # Prepends unless already present. 72 def prepend_unless_present(already_present: Bool, target_key: String, staged_keys: List<&2, String>) -> List<&2, String>: 73 match already_present: 74 case True{}: 75 staged_keys 76 case False{}: 77 Con{target_key, staged_keys} 78 79 # Adds the key unless the manifest has it. 80 def add_key_unless_present(+target_key: String, +staged_keys: List<&2, String>) -> List<&2, String>: 81 prepend_unless_present(contains_staged_key(target_key, staged_keys), target_key, staged_keys) 82 83 # Loads the session manifest. 84 def load_stage_index(+session_identifier: String) -> Store.Op<&2, List<&2, String>>: 85 do Store.Op<&2, List<&2, String>>: 86 stored_maybe : Maybe<&2, String> <- Store.oget(stage_index_key(session_identifier)) 87 return parse_stage_index(stored_maybe) 88 89 # Stores one staged value and records its key. 90 def record_stage_entry(+session_identifier: String, +namespace_name: String, +record_identifier: String, value_hash: String) -> Store.Op<&2, LogicalKey.LogicalKey>: 91 do Store.Op<&2, LogicalKey.LogicalKey>: 92 +full_key_text : String = LogicalKey.encode_logical_key(LogicalKey.Make{namespace_name, record_identifier}) 93 current_index : List<&2, String> <- load_stage_index(session_identifier) 94 stored_unit : Unit <- Store.oput(LogicalKey.build_stage_storage_key(session_identifier, namespace_name, record_identifier), value_hash) 95 saved_unit : Unit <- Store.oput(stage_index_key(session_identifier), serialize_key_list(add_key_unless_present(full_key_text, current_index))) 96 return LogicalKey.Make{namespace_name, record_identifier} 97 98 # Stages a value for later commit. 99 def put_record(+session_identifier: String, +namespace_name: String, +record_identifier: String, value: Value.Value) -> Store.Op<&2, LogicalKey.LogicalKey>: 100 do Store.Op<&2, LogicalKey.LogicalKey>: 101 +value_hash : String <- ContentObject.store_content_object(value, Nil{}) 102 staged_key : LogicalKey.LogicalKey <- record_stage_entry(session_identifier, namespace_name, record_identifier, value_hash) 103 return staged_key 104 105 # Stages a tombstone for later commit. 106 def delete_record(+session_identifier: String, +namespace_name: String, +record_identifier: String) -> Store.Op<&2, LogicalKey.LogicalKey>: 107 do Store.Op<&2, LogicalKey.LogicalKey>: 108 staged_key : LogicalKey.LogicalKey <- record_stage_entry(session_identifier, namespace_name, record_identifier, "TOMBSTONE") 109 return staged_key