src/History.bend
ber-core-store@0.1.1.0 · 495 lines · 0 definitions
1import Base
2import ber-core-store@0.1.1.0/src/Reading.bend as Reading
3import ber-core-store@0.1.1.0/src/LogicalKey.bend as LogicalKey
4import ber-core-store@0.1.1.0/src/ContentHash.bend as ContentHash
5import ber-core-store@0.1.1.0/src/StateTree.bend as StateTree
6import ber-core-store@0.1.1.0/src/Staging.bend as Staging
7import ber-core-store@0.1.1.0/src/ContentObject.bend as ContentObject
8import ber-core-store@0.1.1.0/src/JsonAdapter.bend as JsonAdapter
9import ber-core-store@0.1.1.0/src/Value.bend as Value
10import ber-core-store@0.1.1.0/src/Store.bend as Store
11import 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.
21type CommitMeta is Data:
22 Field{field_key: String, field_value: String}
23
24# Identifier, parents, tree hash, timestamp, metadata and certificate.
25type 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.
29def 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.
35def 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.
41def 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.
47def 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.
55def 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.
63def 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.
67def 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.
71def 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.
79def 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.
87def 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.
95def 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.
103def 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.
111def 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.
115def 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.
123def 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.
131def 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.
139def 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.
147def 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.
155def 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.
163def 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.
171def 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.
179def 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.
187def 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.
195def 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.
199def 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.
207def 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.
211def 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.
219def 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.
225def empty_unit() -> Store.Op<&2, Unit>:
226 do Store.Op<&2, Unit>:
227 return Unit{}
228
229# Empty Sess result of entry-list type.
230def 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.
235def 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.
240def 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.
248def 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.
255def 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.
266def 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.
277def 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.
285def 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.
293def 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.
301def 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.
307def 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.
315def 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.
322def 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.
330def 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.
336def 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.
344def 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.
352def 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.
359def 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.
367def 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.
374def 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.
383def 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.
389def 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.
394def 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.
399def 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).
404def 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.
409def 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.
417def 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# Walks first-parents with fuel; newest first.
425def walk_commit_chain(remaining_fuel: Nat, +commit_identifier: String) -> Store.Op<&2, List<&2, String>>:
426 match remaining_fuel:
427 case 0n:
428 empty_string_list()
429 case 1n+fuel_left:
430 do Store.Op<&2, List<&2, String>>:
431 commit_maybe : Maybe<&2, Commit> <- load_commit(commit_identifier)
432 tail_chain : List<&2, String> <- walk_commit_chain(fuel_left, first_parent_text(commit_maybe))
433 return Con{commit_identifier, tail_chain}
434
435# A stored value wins; otherwise the tail result.
436def decide_search_hit(stored_maybe: Maybe<&2, String>, +lookup_key: String, tail_result: Maybe<&2, Reading.ChainEntry>) -> Maybe<&2, Reading.ChainEntry>:
437 match stored_maybe:
438 case None{}:
439 tail_result
440 case Some{stored_value}:
441 Some{Reading.make_entry(lookup_key, stored_value)}
442
443# Searches the ancestor chain newest-first.
444def search_chain_entries(ancestor_chain: List<&2, String>, +namespace_name: String, +record_identifier: String) -> Store.Op<&2, Maybe<&2, Reading.ChainEntry>>:
445 match ancestor_chain:
446 case Nil{}:
447 empty_entry_maybe()
448 case Con{head_commit, remaining_commits}:
449 do Store.Op<&2, Maybe<&2, Reading.ChainEntry>>:
450 +lookup_key : String = Reading.encode_lookup_key(namespace_name, record_identifier)
451 stored_maybe : Maybe<&2, String> <- Store.oget(LogicalKey.build_version_index_key(lookup_key, head_commit))
452 tail_result : Maybe<&2, Reading.ChainEntry> <- search_chain_entries(remaining_commits, namespace_name, record_identifier)
453 return decide_search_hit(stored_maybe, lookup_key, tail_result)
454
455# Projects the payload out of a loaded object.
456def payload_or_empty(loaded_maybe: Maybe<&2, ContentObject.ContentObject>) -> Maybe<&2, Value.Value>:
457 match loaded_maybe:
458 case None{}:
459 None{}
460 case Some{loaded_object}:
461 Some{ContentObject.object_payload_of(loaded_object)}
462
463# Loads an object payload by content hash.
464def load_object_payload(+value_hash: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
465 do Store.Op<&2, Maybe<&2, Value.Value>>:
466 loaded_maybe : Maybe<&2, ContentObject.ContentObject> <- ContentObject.load_content_object(value_hash)
467 return payload_or_empty(loaded_maybe)
468
469# Tombstones read as absent; hashes load payloads.
470def decide_resolve_value(is_tombstone: Bool, +stored_value: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
471 match is_tombstone:
472 case True{}:
473 empty_value_maybe()
474 case False{}:
475 load_object_payload(stored_value)
476
477# Resolves one stored index value.
478def resolve_entry_value(+stored_value: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
479 decide_resolve_value(String.eq(stored_value, "TOMBSTONE"), stored_value)
480
481# Resolves a found chain entry to its payload.
482def resolve_found_entry(found_maybe: Maybe<&2, Reading.ChainEntry>) -> Store.Op<&2, Maybe<&2, Value.Value>>:
483 match found_maybe:
484 case None{}:
485 empty_value_maybe()
486 case Some{found_entry}:
487 resolve_entry_value(Reading.entry_value_of(found_entry))
488
489# Reads a namespaced value at a commit.
490def read_value_at(+namespace_name: String, +record_identifier: String, +commit_identifier: String) -> Store.Op<&2, Maybe<&2, Value.Value>>:
491 do Store.Op<&2, Maybe<&2, Value.Value>>:
492 ancestor_chain : List<&2, String> <- walk_commit_chain(10000n, commit_identifier)
493 found_entry : Maybe<&2, Reading.ChainEntry> <- search_chain_entries(ancestor_chain, namespace_name, record_identifier)
494 resolved_value : Maybe<&2, Value.Value> <- resolve_found_entry(found_entry)
495 return resolved_value