@peerbit/native-backbone 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ use js_sys::{Array, Reflect, Uint8Array};
2
+ use peerbit_indexer_core::wire::{self, WireError};
3
+ use std::collections::HashSet;
4
+ use wasm_bindgen::prelude::*;
5
+ use wasm_bindgen::JsCast;
6
+
7
+ /// Drops only the first `byte_len` bytes / `record_count` records so records
8
+ /// appended while a flush was awaiting its disk write are kept for the next
9
+ /// flush instead of being discarded.
10
+ pub(crate) fn clear_journal_prefix(
11
+ journal: &mut Vec<u8>,
12
+ journal_record_count: &mut usize,
13
+ byte_len: usize,
14
+ record_count: usize,
15
+ ) {
16
+ if byte_len >= journal.len() {
17
+ journal.clear();
18
+ *journal_record_count = 0;
19
+ return;
20
+ }
21
+ journal.drain(..byte_len);
22
+ *journal_record_count = journal_record_count.saturating_sub(record_count);
23
+ }
24
+
25
+ pub(crate) fn array_from_value(value: JsValue, label: &str) -> Result<Array, JsValue> {
26
+ value
27
+ .dyn_into::<Array>()
28
+ .map_err(|_| JsValue::from_str(&format!("Expected {label} array")))
29
+ }
30
+
31
+ pub(crate) fn string_field(row: &Array, index: u32, label: &str) -> Result<String, JsValue> {
32
+ row.get(index)
33
+ .as_string()
34
+ .ok_or_else(|| JsValue::from_str(&format!("Expected {label} string")))
35
+ }
36
+
37
+ pub(crate) fn stringish_field(row: &Array, index: u32, label: &str) -> Result<String, JsValue> {
38
+ let value = row.get(index);
39
+ if let Some(value) = value.as_string() {
40
+ return Ok(value);
41
+ }
42
+ if let Some(value) = value.as_f64() {
43
+ return Ok((value as u64).to_string());
44
+ }
45
+ Err(JsValue::from_str(&format!("Expected {label} string")))
46
+ }
47
+
48
+ pub(crate) fn bool_field(row: &Array, index: u32, label: &str) -> Result<bool, JsValue> {
49
+ row.get(index)
50
+ .as_bool()
51
+ .ok_or_else(|| JsValue::from_str(&format!("Expected {label} boolean")))
52
+ }
53
+
54
+ pub(crate) fn usize_field(row: &Array, index: u32, label: &str) -> Result<usize, JsValue> {
55
+ row.get(index)
56
+ .as_f64()
57
+ .map(|value| value as usize)
58
+ .ok_or_else(|| JsValue::from_str(&format!("Expected {label} number")))
59
+ }
60
+
61
+ pub(crate) fn bytes_field(row: &Array, index: u32, label: &str) -> Result<Vec<u8>, JsValue> {
62
+ let value = row.get(index);
63
+ if value.is_undefined() || value.is_null() {
64
+ return Err(JsValue::from_str(&format!("Expected {label} bytes")));
65
+ }
66
+ Ok(Uint8Array::new(&value).to_vec())
67
+ }
68
+
69
+ pub(crate) fn trim_hashes_vec(trim_rows: &Array) -> Result<Vec<String>, JsValue> {
70
+ let mut hashes = Vec::with_capacity(trim_rows.length() as usize);
71
+ for index in 0..trim_rows.length() {
72
+ let row = array_from_value(trim_rows.get(index), "trim row")?;
73
+ hashes.push(string_field(&row, 0, "trim hash")?);
74
+ }
75
+ Ok(hashes)
76
+ }
77
+
78
+ pub(crate) fn numbers_to_rows(resolution: &str, values: &[u64]) -> Array {
79
+ let out = Array::new();
80
+ for value in values {
81
+ out.push(&number_to_row(resolution, *value));
82
+ }
83
+ out
84
+ }
85
+
86
+ pub(crate) fn number_to_row(resolution: &str, value: u64) -> JsValue {
87
+ match resolution {
88
+ "u64" => JsValue::from_str(&value.to_string()),
89
+ _ => JsValue::from_f64(value as f64),
90
+ }
91
+ }
92
+
93
+ pub(crate) fn strings_to_array(values: Vec<String>) -> Array {
94
+ let out = Array::new();
95
+ for value in values {
96
+ out.push(&JsValue::from_str(&value));
97
+ }
98
+ out
99
+ }
100
+
101
+ pub(crate) fn has_duplicate_strings(values: &[String]) -> bool {
102
+ let mut seen = HashSet::with_capacity(values.len());
103
+ values.iter().any(|value| !seen.insert(value))
104
+ }
105
+
106
+ pub(crate) fn strings_slice_to_array(values: &[String]) -> Array {
107
+ let out = Array::new();
108
+ for value in values {
109
+ out.push(&JsValue::from_str(value));
110
+ }
111
+ out
112
+ }
113
+
114
+ pub(crate) fn strings_from_array(values: Array) -> Result<Vec<String>, JsValue> {
115
+ let mut out = Vec::with_capacity(values.length() as usize);
116
+ for index in 0..values.length() {
117
+ out.push(
118
+ values
119
+ .get(index)
120
+ .as_string()
121
+ .ok_or_else(|| JsValue::from_str("Expected string array"))?,
122
+ );
123
+ }
124
+ Ok(out)
125
+ }
126
+
127
+ pub(crate) fn bytes_vec_from_array(values: Array) -> Result<Vec<Vec<u8>>, JsValue> {
128
+ let mut out = Vec::with_capacity(values.length() as usize);
129
+ for index in 0..values.length() {
130
+ let value = values.get(index);
131
+ if value.is_undefined() || value.is_null() {
132
+ return Err(JsValue::from_str("Expected bytes array"));
133
+ }
134
+ out.push(Uint8Array::new(&value).to_vec());
135
+ }
136
+ Ok(out)
137
+ }
138
+
139
+ pub(crate) fn required_bytes_from_array(
140
+ values: &Array,
141
+ index: u32,
142
+ field: &str,
143
+ ) -> Result<Uint8Array, JsValue> {
144
+ let value = values.get(index);
145
+ if value.is_undefined() || value.is_null() {
146
+ return Err(JsValue::from_str(&format!("Expected {field} bytes")));
147
+ }
148
+ Ok(Uint8Array::new(&value))
149
+ }
150
+
151
+ pub(crate) fn string_batches_from_array(
152
+ values: Array,
153
+ label: &str,
154
+ ) -> Result<Vec<Vec<String>>, JsValue> {
155
+ let mut out = Vec::with_capacity(values.length() as usize);
156
+ for index in 0..values.length() {
157
+ let value = values.get(index);
158
+ if !Array::is_array(&value) {
159
+ return Err(JsValue::from_str(&format!("Expected {label}")));
160
+ }
161
+ out.push(strings_from_array(Array::from(&value))?);
162
+ }
163
+ Ok(out)
164
+ }
165
+
166
+ pub(crate) fn usize_values_from_array(values: Array) -> Result<Vec<usize>, JsValue> {
167
+ let mut out = Vec::with_capacity(values.length() as usize);
168
+ for index in 0..values.length() {
169
+ let value = values
170
+ .get(index)
171
+ .as_f64()
172
+ .ok_or_else(|| JsValue::from_str("Expected unsigned integer array"))?;
173
+ if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
174
+ return Err(JsValue::from_str("Expected unsigned integer array"));
175
+ }
176
+ out.push(value as usize);
177
+ }
178
+ Ok(out)
179
+ }
180
+
181
+ pub(crate) fn ensure_same_len(left: usize, right: usize, label: &str) -> Result<(), JsValue> {
182
+ if left == right {
183
+ Ok(())
184
+ } else {
185
+ Err(JsValue::from_str(&format!(
186
+ "Mismatched {label} input lengths"
187
+ )))
188
+ }
189
+ }
190
+
191
+ pub(crate) fn optional_bytes_from_js(value: JsValue) -> Option<Vec<u8>> {
192
+ if value.is_undefined() || value.is_null() {
193
+ return None;
194
+ }
195
+ Some(Uint8Array::new(&value).to_vec())
196
+ }
197
+
198
+ pub(crate) fn optional_usize_from_js(
199
+ value: JsValue,
200
+ label: &str,
201
+ ) -> Result<Option<usize>, JsValue> {
202
+ if value.is_undefined() || value.is_null() {
203
+ return Ok(None);
204
+ }
205
+ value
206
+ .as_f64()
207
+ .map(|value| Some(value as usize))
208
+ .ok_or_else(|| JsValue::from_str(&format!("{label} must be a number")))
209
+ }
210
+
211
+ pub(crate) fn number_strings_to_array(values: &[u64]) -> Array {
212
+ let out = Array::new();
213
+ for value in values {
214
+ out.push(&JsValue::from_str(&value.to_string()));
215
+ }
216
+ out
217
+ }
218
+
219
+ pub(crate) fn parse_u64_string(value: &str, label: &str) -> Result<u64, JsValue> {
220
+ value
221
+ .parse::<u64>()
222
+ .map_err(|_| JsValue::from_str(&format!("Expected {label} u64 string")))
223
+ }
224
+
225
+ pub(crate) fn parse_optional_u64_string(value: &str, label: &str) -> Result<Option<u64>, JsValue> {
226
+ if value.is_empty() {
227
+ Ok(None)
228
+ } else {
229
+ parse_u64_string(value, label).map(Some)
230
+ }
231
+ }
232
+
233
+ const JOURNAL_PUT_OPERATION: u8 = 1;
234
+ const JOURNAL_DELETE_OPERATION: u8 = 2;
235
+
236
+ /// Writes the same wire format as
237
+ /// `peerbit_indexer_core::persistence::encode_journal_put_record`, but
238
+ /// directly into `out` — the journal push runs once per append transaction,
239
+ /// so the intermediate payload/record allocations are avoided on purpose
240
+ /// (see the `journal record encoding matches indexer core` parity test).
241
+ pub(crate) fn append_journal_put_record(out: &mut Vec<u8>, key: &str, value: &[u8]) {
242
+ let payload_len = 1usize + 4 + key.len() + 4 + value.len();
243
+ let key_len = (key.len() as u32).to_le_bytes();
244
+ let value_len = (value.len() as u32).to_le_bytes();
245
+ let checksum = fnv1a_parts([
246
+ &[JOURNAL_PUT_OPERATION][..],
247
+ key_len.as_slice(),
248
+ key.as_bytes(),
249
+ value_len.as_slice(),
250
+ value,
251
+ ]);
252
+ out.reserve(8 + payload_len);
253
+ out.extend_from_slice(&(payload_len as u32).to_le_bytes());
254
+ out.extend_from_slice(&checksum.to_le_bytes());
255
+ out.push(JOURNAL_PUT_OPERATION);
256
+ out.extend_from_slice(&key_len);
257
+ out.extend_from_slice(key.as_bytes());
258
+ out.extend_from_slice(&value_len);
259
+ out.extend_from_slice(value);
260
+ }
261
+
262
+ /// Allocation-free equivalent of
263
+ /// `peerbit_indexer_core::persistence::encode_journal_delete_record`.
264
+ pub(crate) fn append_journal_delete_record(out: &mut Vec<u8>, key: &str) {
265
+ let payload_len = 1usize + 4 + key.len();
266
+ let key_len = (key.len() as u32).to_le_bytes();
267
+ let checksum = fnv1a_parts([
268
+ &[JOURNAL_DELETE_OPERATION][..],
269
+ key_len.as_slice(),
270
+ key.as_bytes(),
271
+ ]);
272
+ out.reserve(8 + payload_len);
273
+ out.extend_from_slice(&(payload_len as u32).to_le_bytes());
274
+ out.extend_from_slice(&checksum.to_le_bytes());
275
+ out.push(JOURNAL_DELETE_OPERATION);
276
+ out.extend_from_slice(&key_len);
277
+ out.extend_from_slice(key.as_bytes());
278
+ }
279
+
280
+ /// FNV-1a over concatenated parts; must stay identical to
281
+ /// `peerbit_indexer_core::persistence::fnv1a` over the joined bytes.
282
+ fn fnv1a_parts<const N: usize>(parts: [&[u8]; N]) -> u32 {
283
+ let mut hash = 0x811c9dc5u32;
284
+ for part in parts {
285
+ for byte in part {
286
+ hash ^= u32::from(*byte);
287
+ hash = hash.wrapping_mul(0x01000193);
288
+ }
289
+ }
290
+ hash
291
+ }
292
+
293
+ pub(crate) fn write_string(out: &mut Vec<u8>, value: &str) {
294
+ out.extend_from_slice(&(value.len() as u32).to_le_bytes());
295
+ out.extend_from_slice(value.as_bytes());
296
+ }
297
+
298
+ pub(crate) fn write_u64(out: &mut Vec<u8>, value: u64) {
299
+ out.extend_from_slice(&value.to_le_bytes());
300
+ }
301
+
302
+ pub(crate) fn write_u32(out: &mut Vec<u8>, value: u32) {
303
+ out.extend_from_slice(&value.to_le_bytes());
304
+ }
305
+
306
+ pub(crate) fn write_bytes(out: &mut Vec<u8>, value: &[u8]) {
307
+ out.extend_from_slice(&(value.len() as u32).to_le_bytes());
308
+ out.extend_from_slice(value);
309
+ }
310
+
311
+ pub(crate) fn wire_error_to_js(error: WireError) -> JsValue {
312
+ JsValue::from_str(&error.to_string())
313
+ }
314
+
315
+ pub(crate) fn read_u32(
316
+ bytes: &[u8],
317
+ offset: &mut usize,
318
+ label: &'static str,
319
+ ) -> Result<u32, JsValue> {
320
+ wire::read_u32(bytes, offset, label).map_err(wire_error_to_js)
321
+ }
322
+
323
+ pub(crate) fn read_u64(
324
+ bytes: &[u8],
325
+ offset: &mut usize,
326
+ label: &'static str,
327
+ ) -> Result<u64, JsValue> {
328
+ wire::read_u64(bytes, offset, label).map_err(wire_error_to_js)
329
+ }
330
+
331
+ pub(crate) fn read_encoded_string(
332
+ bytes: &[u8],
333
+ offset: &mut usize,
334
+ label: &'static str,
335
+ ) -> Result<String, JsValue> {
336
+ wire::read_encoded_string(bytes, offset, label).map_err(wire_error_to_js)
337
+ }
338
+
339
+ pub(crate) fn read_bytes(
340
+ bytes: &[u8],
341
+ offset: &mut usize,
342
+ label: &'static str,
343
+ ) -> Result<Vec<u8>, JsValue> {
344
+ wire::read_bytes(bytes, offset, label).map_err(wire_error_to_js)
345
+ }
346
+
347
+ pub(crate) fn js_get(value: &JsValue, key: &str) -> JsValue {
348
+ Reflect::get(value, &JsValue::from_str(key)).unwrap_or(JsValue::UNDEFINED)
349
+ }
350
+
351
+ fn js_string(value: JsValue, field: &str) -> Result<String, JsValue> {
352
+ value
353
+ .as_string()
354
+ .ok_or_else(|| JsValue::from_str(&format!("Missing or invalid {field}")))
355
+ }
356
+
357
+ pub(crate) fn array_strings(value: JsValue, field: &str) -> Result<Vec<String>, JsValue> {
358
+ if !Array::is_array(&value) {
359
+ return Err(JsValue::from_str(&format!("{field} must be an array")));
360
+ }
361
+ let array = Array::from(&value);
362
+ let mut out = Vec::with_capacity(array.length() as usize);
363
+ for index in 0..array.length() {
364
+ out.push(js_string(array.get(index), field)?);
365
+ }
366
+ Ok(out)
367
+ }
368
+
369
+ pub(crate) fn optional_string(value: JsValue) -> Option<String> {
370
+ if value.is_null() || value.is_undefined() {
371
+ None
372
+ } else {
373
+ value.as_string()
374
+ }
375
+ }
376
+
377
+ pub(crate) fn write_u8(out: &mut Vec<u8>, value: u8) {
378
+ out.push(value);
379
+ }
380
+
381
+ pub(crate) fn write_bool(out: &mut Vec<u8>, value: bool) {
382
+ out.push(if value { 1 } else { 0 });
383
+ }
384
+
385
+ pub(crate) fn js_error(error: impl std::fmt::Display) -> JsValue {
386
+ JsValue::from_str(&error.to_string())
387
+ }
388
+
389
+ pub(crate) fn decode_error(error: impl std::fmt::Display) -> JsValue {
390
+ JsValue::from_str(&error.to_string())
391
+ }
392
+
393
+ pub(crate) fn hash_number_u64(resolution: &str, digest: &[u8]) -> Result<u64, JsValue> {
394
+ match resolution {
395
+ "u32" => {
396
+ if digest.len() < 4 {
397
+ return Err(JsValue::from_str("hash digest must have at least 4 bytes"));
398
+ }
399
+ Ok(u32::from_le_bytes(digest[0..4].try_into().unwrap()) as u64)
400
+ }
401
+ "u64" => {
402
+ if digest.len() < 8 {
403
+ return Err(JsValue::from_str("hash digest must have at least 8 bytes"));
404
+ }
405
+ Ok(u64::from_le_bytes(digest[0..8].try_into().unwrap()))
406
+ }
407
+ _ => Err(JsValue::from_str("resolution must be u32 or u64")),
408
+ }
409
+ }
410
+
411
+ #[cfg(test)]
412
+ mod tests {
413
+ use super::{append_journal_delete_record, append_journal_put_record, hash_number_u64};
414
+ use peerbit_indexer_core::persistence::{
415
+ encode_journal_delete_record, encode_journal_put_record,
416
+ };
417
+
418
+ #[test]
419
+ fn decodes_hash_numbers_like_shared_log_integer_helpers() {
420
+ let bytes = [1, 0, 0, 0, 2, 0, 0, 0];
421
+ assert_eq!(hash_number_u64("u32", &bytes).unwrap(), 1);
422
+ assert_eq!(hash_number_u64("u64", &bytes).unwrap(), 8_589_934_593);
423
+ }
424
+
425
+ #[test]
426
+ fn journal_record_encoding_matches_indexer_core() {
427
+ for (key, value) in [
428
+ ("", &[][..]),
429
+ ("k", &[0u8][..]),
430
+ ("some-journal-key", &[1u8, 2, 3, 255, 0, 42][..]),
431
+ ("zAbc123", &[7u8; 1200][..]),
432
+ ] {
433
+ let mut direct = vec![9u8, 9, 9];
434
+ append_journal_put_record(&mut direct, key, value);
435
+ let mut expected = vec![9u8, 9, 9];
436
+ expected.extend_from_slice(&encode_journal_put_record(key, value));
437
+ assert_eq!(direct, expected, "put record for key {key:?}");
438
+
439
+ let mut direct = Vec::new();
440
+ append_journal_delete_record(&mut direct, key);
441
+ assert_eq!(
442
+ direct,
443
+ encode_journal_delete_record(key),
444
+ "delete record for key {key:?}"
445
+ );
446
+ }
447
+ }
448
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,144 @@
1
+ use js_sys::{Array, Uint8Array};
2
+ use peerbit_indexer_core::planner::NativeQueryIndex;
3
+ use peerbit_indexer_core::schema::NativeSchemaIr;
4
+ use peerbit_indexer_core::storage::MemoryByteStorage;
5
+ use peerbit_log_rust::{NativeEntryV0PlainBuilder, NativeLogBlockStore, NativeLogIndex};
6
+ use peerbit_shared_log_rust::NativeSharedLogState;
7
+ use std::collections::{HashMap, HashSet};
8
+ use wasm_bindgen::prelude::*;
9
+
10
+ mod append_tx;
11
+ mod coordinates;
12
+ mod documents;
13
+ mod graph_blocks;
14
+ mod js_interop;
15
+ mod profile;
16
+ mod raw_receive;
17
+ mod shared_log_plan;
18
+ mod sync_send;
19
+ mod wire_sync;
20
+
21
+ use crate::documents::{DocumentContextFields, DocumentPreviousSignerFact, ParsedProjectionPlan};
22
+ use crate::profile::NativeBackboneAppendProfile;
23
+ use crate::raw_receive::PendingRawReceiveEntry;
24
+
25
+ // Native-backbone is optimized for document-store hot paths where large byte
26
+ // fields are payloads, not query keys. Standalone indexer-rust keeps exact large
27
+ // byte matching through the compatibility extractor.
28
+ const NATIVE_BACKBONE_BYTE_EXACT_INDEX_LIMIT: usize = 128;
29
+
30
+ #[wasm_bindgen]
31
+ pub struct NativePeerbitBackbone {
32
+ resolution: String,
33
+ log: NativeLogIndex,
34
+ blocks: NativeLogBlockStore,
35
+ shared_log: NativeSharedLogState,
36
+ coordinate_index: HashSet<String>,
37
+ coordinate_values: MemoryByteStorage,
38
+ coordinate_journal: Vec<u8>,
39
+ coordinate_journal_record_count: usize,
40
+ coordinate_journal_enabled: bool,
41
+ document_index: NativeQueryIndex,
42
+ document_values: MemoryByteStorage,
43
+ document_journal: Vec<u8>,
44
+ document_journal_record_count: usize,
45
+ document_journal_enabled: bool,
46
+ document_byte_element_index_limit: usize,
47
+ document_key_by_head: HashMap<String, String>,
48
+ document_previous_signer_by_key: HashMap<String, DocumentPreviousSignerFact>,
49
+ document_signer_journal: Vec<u8>,
50
+ document_signer_journal_record_count: usize,
51
+ document_signer_journal_enabled: bool,
52
+ document_schema_ir: Option<NativeSchemaIr>,
53
+ document_context_head_field: Option<u32>,
54
+ document_context_fields: Option<DocumentContextFields>,
55
+ document_projection_plans: Vec<ParsedProjectionPlan>,
56
+ local_public_key: Vec<u8>,
57
+ builder: NativeEntryV0PlainBuilder,
58
+ pending_raw_receive_entries: HashMap<String, PendingRawReceiveEntry>,
59
+ append_profile_enabled: bool,
60
+ append_profile: NativeBackboneAppendProfile,
61
+ }
62
+
63
+ #[wasm_bindgen]
64
+ impl NativePeerbitBackbone {
65
+ #[wasm_bindgen(constructor)]
66
+ pub fn new(
67
+ resolution: String,
68
+ clock_id: Uint8Array,
69
+ private_key: Uint8Array,
70
+ public_key: Uint8Array,
71
+ ) -> Result<Self, JsValue> {
72
+ if resolution != "u32" && resolution != "u64" {
73
+ return Err(JsValue::from_str("resolution must be u32 or u64"));
74
+ }
75
+ let local_public_key = public_key.to_vec();
76
+ Ok(Self {
77
+ resolution: resolution.clone(),
78
+ log: NativeLogIndex::new(),
79
+ blocks: NativeLogBlockStore::new(),
80
+ shared_log: NativeSharedLogState::new(resolution),
81
+ coordinate_index: HashSet::new(),
82
+ coordinate_values: MemoryByteStorage::new(),
83
+ coordinate_journal: Vec::new(),
84
+ coordinate_journal_record_count: 0,
85
+ coordinate_journal_enabled: false,
86
+ document_index: NativeQueryIndex::new(),
87
+ document_values: MemoryByteStorage::new(),
88
+ document_journal: Vec::new(),
89
+ document_journal_record_count: 0,
90
+ document_journal_enabled: false,
91
+ document_byte_element_index_limit: 0,
92
+ document_key_by_head: HashMap::new(),
93
+ document_previous_signer_by_key: HashMap::new(),
94
+ document_signer_journal: Vec::new(),
95
+ document_signer_journal_record_count: 0,
96
+ document_signer_journal_enabled: false,
97
+ document_schema_ir: None,
98
+ document_context_head_field: None,
99
+ document_context_fields: None,
100
+ document_projection_plans: Vec::new(),
101
+ local_public_key,
102
+ builder: NativeEntryV0PlainBuilder::new(clock_id, private_key, public_key)?,
103
+ pending_raw_receive_entries: HashMap::new(),
104
+ append_profile_enabled: false,
105
+ append_profile: NativeBackboneAppendProfile::default(),
106
+ })
107
+ }
108
+
109
+ pub fn set_append_profile_enabled(&mut self, enabled: bool) {
110
+ self.append_profile_enabled = enabled;
111
+ }
112
+
113
+ pub fn reset_append_profile(&mut self) {
114
+ self.append_profile = NativeBackboneAppendProfile::default();
115
+ }
116
+
117
+ pub fn append_profile(&self) -> Array {
118
+ self.append_profile.to_row()
119
+ }
120
+
121
+ pub fn log_len(&self) -> usize {
122
+ self.log.len()
123
+ }
124
+
125
+ pub fn block_len(&self) -> usize {
126
+ self.blocks.len()
127
+ }
128
+
129
+ pub fn has_log_entry(&self, hash: &str) -> bool {
130
+ self.log.has(hash)
131
+ }
132
+
133
+ pub fn has_block(&self, hash: &str) -> bool {
134
+ self.blocks.has(hash)
135
+ }
136
+
137
+ pub fn clear(&mut self) {
138
+ self.log.clear();
139
+ self.blocks.clear();
140
+ self.shared_log.clear();
141
+ self.clear_coordinate_core();
142
+ self.clear_document_core();
143
+ }
144
+ }