@peerbit/native-backbone 0.1.1 → 0.1.3

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.
@@ -6,6 +6,7 @@ use crate::append_tx::coordinate_plan_to_row;
6
6
  use crate::documents::{
7
7
  document_context_facts_to_row, document_index_append_commit, DocumentIndexAppendCommit,
8
8
  };
9
+ use crate::error::BackboneError;
9
10
  use crate::js_interop::{
10
11
  array_from_value, bytes_field, string_field, strings_from_array, strings_to_array,
11
12
  trim_hashes_vec,
@@ -13,6 +14,18 @@ use crate::js_interop::{
13
14
  use crate::shared_log_plan::leader_samples_to_optional_rows;
14
15
  use crate::NativePeerbitBackbone;
15
16
 
17
+ /// Forward a JsValue error from an untyped log-rust wrapper without altering
18
+ /// its message. These wrappers construct every error via `JsValue::from_str`,
19
+ /// so `as_string()` recovers the exact string the wrapper would have thrown;
20
+ /// the fallback only guards the theoretically-non-string case.
21
+ fn js_wrapper_error(error: JsValue) -> BackboneError {
22
+ BackboneError::Message(
23
+ error
24
+ .as_string()
25
+ .unwrap_or_else(|| "Expected string array".to_string()),
26
+ )
27
+ }
28
+
16
29
  #[wasm_bindgen]
17
30
  impl NativePeerbitBackbone {
18
31
  #[allow(clippy::too_many_arguments)]
@@ -81,7 +94,7 @@ impl NativePeerbitBackbone {
81
94
  self_replicating: bool,
82
95
  resolve_trimmed_entries: bool,
83
96
  ) -> Result<Array, JsValue> {
84
- self.prepare_plain_storage_append_transaction_inner(
97
+ Ok(self.prepare_plain_storage_append_transaction_inner(
85
98
  wall_time,
86
99
  logical,
87
100
  gid,
@@ -98,7 +111,7 @@ impl NativePeerbitBackbone {
98
111
  None,
99
112
  false,
100
113
  None,
101
- )
114
+ )?)
102
115
  }
103
116
 
104
117
  #[allow(clippy::too_many_arguments)]
@@ -118,7 +131,7 @@ impl NativePeerbitBackbone {
118
131
  resolve_trimmed_entries: bool,
119
132
  trim_length_to: usize,
120
133
  ) -> Result<Array, JsValue> {
121
- self.prepare_plain_storage_append_transaction_inner(
134
+ Ok(self.prepare_plain_storage_append_transaction_inner(
122
135
  wall_time,
123
136
  logical,
124
137
  gid,
@@ -135,7 +148,7 @@ impl NativePeerbitBackbone {
135
148
  Some(trim_length_to),
136
149
  false,
137
150
  None,
138
- )
151
+ )?)
139
152
  }
140
153
 
141
154
  #[allow(clippy::too_many_arguments)]
@@ -162,7 +175,7 @@ impl NativePeerbitBackbone {
162
175
  document_projection_encoded_document: JsValue,
163
176
  document_projection_signer: JsValue,
164
177
  ) -> Result<Array, JsValue> {
165
- self.prepare_plain_storage_append_transaction_inner(
178
+ Ok(self.prepare_plain_storage_append_transaction_inner(
166
179
  wall_time,
167
180
  logical,
168
181
  gid,
@@ -188,7 +201,7 @@ impl NativePeerbitBackbone {
188
201
  document_projection_encoded_document,
189
202
  document_projection_signer,
190
203
  )?),
191
- )
204
+ )?)
192
205
  }
193
206
 
194
207
  #[allow(clippy::too_many_arguments)]
@@ -216,7 +229,7 @@ impl NativePeerbitBackbone {
216
229
  document_projection_signer: JsValue,
217
230
  trim_length_to: usize,
218
231
  ) -> Result<Array, JsValue> {
219
- self.prepare_plain_storage_append_transaction_inner(
232
+ Ok(self.prepare_plain_storage_append_transaction_inner(
220
233
  wall_time,
221
234
  logical,
222
235
  gid,
@@ -242,7 +255,7 @@ impl NativePeerbitBackbone {
242
255
  document_projection_encoded_document,
243
256
  document_projection_signer,
244
257
  )?),
245
- )
258
+ )?)
246
259
  }
247
260
 
248
261
  #[allow(clippy::too_many_arguments)]
@@ -262,7 +275,7 @@ impl NativePeerbitBackbone {
262
275
  self_replicating: bool,
263
276
  resolve_trimmed_entries: bool,
264
277
  ) -> Result<Array, JsValue> {
265
- self.prepare_plain_storage_append_transaction_inner(
278
+ Ok(self.prepare_plain_storage_append_transaction_inner(
266
279
  wall_time,
267
280
  logical,
268
281
  gid,
@@ -279,7 +292,7 @@ impl NativePeerbitBackbone {
279
292
  None,
280
293
  false,
281
294
  None,
282
- )
295
+ )?)
283
296
  }
284
297
 
285
298
  #[allow(clippy::too_many_arguments)]
@@ -300,7 +313,7 @@ impl NativePeerbitBackbone {
300
313
  resolve_trimmed_entries: bool,
301
314
  trim_length_to: usize,
302
315
  ) -> Result<Array, JsValue> {
303
- self.prepare_plain_storage_append_transaction_inner(
316
+ Ok(self.prepare_plain_storage_append_transaction_inner(
304
317
  wall_time,
305
318
  logical,
306
319
  gid,
@@ -317,7 +330,7 @@ impl NativePeerbitBackbone {
317
330
  Some(trim_length_to),
318
331
  false,
319
332
  None,
320
- )
333
+ )?)
321
334
  }
322
335
 
323
336
  #[allow(clippy::too_many_arguments)]
@@ -345,7 +358,7 @@ impl NativePeerbitBackbone {
345
358
  document_projection_encoded_document: JsValue,
346
359
  document_projection_signer: JsValue,
347
360
  ) -> Result<Array, JsValue> {
348
- self.prepare_plain_storage_append_transaction_inner(
361
+ Ok(self.prepare_plain_storage_append_transaction_inner(
349
362
  wall_time,
350
363
  logical,
351
364
  gid,
@@ -371,7 +384,7 @@ impl NativePeerbitBackbone {
371
384
  document_projection_encoded_document,
372
385
  document_projection_signer,
373
386
  )?),
374
- )
387
+ )?)
375
388
  }
376
389
 
377
390
  #[allow(clippy::too_many_arguments)]
@@ -400,7 +413,7 @@ impl NativePeerbitBackbone {
400
413
  document_projection_signer: JsValue,
401
414
  trim_length_to: usize,
402
415
  ) -> Result<Array, JsValue> {
403
- self.prepare_plain_storage_append_transaction_inner(
416
+ Ok(self.prepare_plain_storage_append_transaction_inner(
404
417
  wall_time,
405
418
  logical,
406
419
  gid,
@@ -426,7 +439,7 @@ impl NativePeerbitBackbone {
426
439
  document_projection_encoded_document,
427
440
  document_projection_signer,
428
441
  )?),
429
- )
442
+ )?)
430
443
  }
431
444
  }
432
445
 
@@ -450,9 +463,9 @@ impl NativePeerbitBackbone {
450
463
  trim_length_to: Option<usize>,
451
464
  commit_blocks: bool,
452
465
  document_index_commit: Option<DocumentIndexAppendCommit>,
453
- ) -> Result<Array, JsValue> {
466
+ ) -> Result<Array, BackboneError> {
454
467
  let profile_enabled = self.append_profile_enabled;
455
- let storage_append_started = profile_enabled.then(js_sys::Date::now);
468
+ let storage_append_started = profile_enabled.then(crate::time::now_ms);
456
469
  let payload_size = payload_data.length();
457
470
  let delete_trimmed_document_heads = document_index_commit
458
471
  .as_ref()
@@ -462,7 +475,7 @@ impl NativePeerbitBackbone {
462
475
  .and_then(|commit| commit.previous_context.clone());
463
476
  let (entry_row, trim_rows, trim_hashes, hash, digest, meta_bytes) = if commit_blocks {
464
477
  let (meta_data, payload_data) =
465
- self.copy_append_inputs_profiled(meta_data, &payload_data);
478
+ self.copy_append_inputs_profiled(meta_data, &payload_data)?;
466
479
  let (entry_facts, trim_hashes, entry_row, trim_rows) = self
467
480
  .prepare_committed_log_append_rows_profiled(
468
481
  wall_time,
@@ -485,6 +498,13 @@ impl NativePeerbitBackbone {
485
498
  )
486
499
  } else if let Some(trim_length_to) = trim_length_to {
487
500
  let next_hashes_array = strings_to_array(next_hashes.clone());
501
+ // No typed log-rust equivalent exists for the storage-facts mode
502
+ // (it hands the storage bytes back to JS instead of committing
503
+ // them to a native block store). It is the one append path with
504
+ // no typed core to re-point to, so its JsValue error is forwarded
505
+ // verbatim through BackboneError::Message, preserving whatever
506
+ // string the wrapper produced (the dominant reachable case is
507
+ // strings_from_array over the array built just above).
488
508
  let row = self
489
509
  .log
490
510
  .prepare_entry_v0_plain_entry_storage_facts_trim_and_put_with_builder(
@@ -497,7 +517,8 @@ impl NativePeerbitBackbone {
497
517
  meta_data,
498
518
  payload_data,
499
519
  trim_length_to,
500
- )?;
520
+ )
521
+ .map_err(js_wrapper_error)?;
501
522
  let row = array_from_value(row.into(), "native storage trim append row")?;
502
523
  let entry_row = array_from_value(row.get(0), "native storage trim append entry row")?;
503
524
  let trim_rows = array_from_value(row.get(1), "native storage trim append trim rows")?;
@@ -508,6 +529,8 @@ impl NativePeerbitBackbone {
508
529
  (entry_row, trim_rows, trim_hashes, hash, digest, meta_bytes)
509
530
  } else {
510
531
  let next_hashes_array = strings_to_array(next_hashes.clone());
532
+ // Same untyped storage-facts wrapper seam as the trim branch
533
+ // above; its JsValue error is forwarded verbatim.
511
534
  let row = self
512
535
  .log
513
536
  .prepare_entry_v0_plain_entry_storage_facts_and_put_with_builder(
@@ -519,7 +542,8 @@ impl NativePeerbitBackbone {
519
542
  entry_type,
520
543
  meta_data,
521
544
  payload_data,
522
- )?;
545
+ )
546
+ .map_err(js_wrapper_error)?;
523
547
  let hash = string_field(&row, 1, "storage entry hash")?;
524
548
  let digest = bytes_field(&row, 5, "storage entry hash digest")?;
525
549
  let meta_bytes = bytes_field(&row, 4, "storage entry meta bytes")?;
@@ -529,7 +553,7 @@ impl NativePeerbitBackbone {
529
553
  let hash_number = self.hash_number_profiled(&digest)?;
530
554
  let document_hash = hash.clone();
531
555
  let document_gid = gid.clone();
532
- let coordinate_plan_started = profile_enabled.then(js_sys::Date::now);
556
+ let coordinate_plan_started = profile_enabled.then(crate::time::now_ms);
533
557
  let coordinate_facts = commit_local_append_for_gid_compact_core(
534
558
  &mut self.shared_log,
535
559
  hash,
@@ -546,9 +570,9 @@ impl NativePeerbitBackbone {
546
570
  true,
547
571
  )?;
548
572
  if let Some(started) = coordinate_plan_started {
549
- self.append_profile.coordinate_plan_ms += js_sys::Date::now() - started;
573
+ self.append_profile.coordinate_plan_ms += crate::time::now_ms() - started;
550
574
  }
551
- let coordinate_core_started = profile_enabled.then(js_sys::Date::now);
575
+ let coordinate_core_started = profile_enabled.then(crate::time::now_ms);
552
576
  self.commit_coordinate_core_from_compact_facts(
553
577
  &coordinate_facts,
554
578
  &next_hashes,
@@ -557,7 +581,7 @@ impl NativePeerbitBackbone {
557
581
  meta_bytes,
558
582
  );
559
583
  if let Some(started) = coordinate_core_started {
560
- self.append_profile.coordinate_core_ms += js_sys::Date::now() - started;
584
+ self.append_profile.coordinate_core_ms += crate::time::now_ms() - started;
561
585
  }
562
586
  let document_trimmed_heads_processed = self.commit_append_document_index_profiled(
563
587
  document_index_commit,
@@ -570,7 +594,7 @@ impl NativePeerbitBackbone {
570
594
  &trim_hashes,
571
595
  )?;
572
596
 
573
- let result_row_started = profile_enabled.then(js_sys::Date::now);
597
+ let result_row_started = profile_enabled.then(crate::time::now_ms);
574
598
  let out = Array::new();
575
599
  out.push(&entry_row);
576
600
  out.push(&leader_samples_to_optional_rows(&coordinate_facts.leaders));
@@ -589,10 +613,10 @@ impl NativePeerbitBackbone {
589
613
  .unwrap_or(JsValue::UNDEFINED),
590
614
  );
591
615
  if let Some(started) = result_row_started {
592
- self.append_profile.result_row_ms += js_sys::Date::now() - started;
616
+ self.append_profile.result_row_ms += crate::time::now_ms() - started;
593
617
  }
594
618
  if let Some(started) = storage_append_started {
595
- self.append_profile.storage_append_inner_ms += js_sys::Date::now() - started;
619
+ self.append_profile.storage_append_inner_ms += crate::time::now_ms() - started;
596
620
  }
597
621
  Ok(out)
598
622
  }
@@ -8,14 +8,16 @@ use peerbit_indexer_core::wire::{self, WireError};
8
8
  use peerbit_shared_log_rust::NativeLocalAppendCompactFacts;
9
9
  use wasm_bindgen::prelude::*;
10
10
 
11
+ use crate::error::BackboneError;
11
12
  use crate::js_interop::{
12
13
  append_journal_delete_record, append_journal_put_record, array_from_value, bool_field,
13
- clear_journal_prefix, decode_error, number_strings_to_array, parse_u64_string, string_field,
14
- stringish_field, strings_from_array, usize_field, wire_error_to_js, write_bytes, write_string,
14
+ clear_journal_prefix, number_strings_to_array, parse_u64_string, string_field, stringish_field,
15
+ strings_from_array, usize_field, write_bytes, write_string,
15
16
  };
16
17
  use crate::shared_log_plan::coordinate_numbers_from_array;
17
18
  use crate::NativePeerbitBackbone;
18
19
 
20
+ #[derive(Debug)]
19
21
  pub(crate) struct CoordinateCoreValue {
20
22
  pub(crate) hash: String,
21
23
  pub(crate) gid: String,
@@ -66,8 +68,43 @@ pub(crate) fn coordinate_core_value_to_row(value: &CoordinateCoreValue) -> Array
66
68
  row
67
69
  }
68
70
 
69
- pub(crate) fn decode_coordinate_value(bytes: &[u8]) -> Result<CoordinateCoreValue, JsValue> {
70
- decode_coordinate_value_core(bytes).map_err(wire_error_to_js)
71
+ pub(crate) fn decode_coordinate_value(bytes: &[u8]) -> Result<CoordinateCoreValue, BackboneError> {
72
+ Ok(decode_coordinate_value_core(bytes)?)
73
+ }
74
+
75
+ /// Decode a key/value snapshot and apply journal records on top, returning
76
+ /// the merged entries in insertion order plus the number of journal
77
+ /// operations applied.
78
+ pub(crate) fn merged_coordinate_entries(
79
+ snapshot: &[u8],
80
+ journal: &[u8],
81
+ ) -> Result<(Vec<(String, Vec<u8>)>, usize), BackboneError> {
82
+ let mut entries = if snapshot.is_empty() {
83
+ Default::default()
84
+ } else {
85
+ decode_key_value_snapshot(snapshot)?
86
+ };
87
+ let journal_records = if journal.is_empty() {
88
+ Vec::new()
89
+ } else {
90
+ decode_journal(journal)?
91
+ };
92
+ let operations = journal_records.len();
93
+ for record in journal_records {
94
+ match record {
95
+ JournalRecord {
96
+ key,
97
+ value: Some(value),
98
+ ..
99
+ } => {
100
+ entries.insert(key, value);
101
+ }
102
+ JournalRecord { key, .. } => {
103
+ entries.shift_remove(&key);
104
+ }
105
+ }
106
+ }
107
+ Ok((entries.into_iter().collect(), operations))
71
108
  }
72
109
 
73
110
  fn decode_coordinate_value_core(bytes: &[u8]) -> Result<CoordinateCoreValue, WireError> {
@@ -178,32 +215,8 @@ impl NativePeerbitBackbone {
178
215
  snapshot: Uint8Array,
179
216
  journal: Uint8Array,
180
217
  ) -> Result<usize, JsValue> {
181
- let mut entries = if snapshot.length() == 0 {
182
- Default::default()
183
- } else {
184
- decode_key_value_snapshot(&snapshot.to_vec()).map_err(decode_error)?
185
- };
186
- let journal_records = if journal.length() == 0 {
187
- Vec::new()
188
- } else {
189
- decode_journal(&journal.to_vec()).map_err(decode_error)?
190
- };
191
- let operations = journal_records.len();
192
- for record in journal_records {
193
- match record {
194
- JournalRecord {
195
- key,
196
- value: Some(value),
197
- ..
198
- } => {
199
- entries.insert(key, value);
200
- }
201
- JournalRecord { key, .. } => {
202
- entries.shift_remove(&key);
203
- }
204
- }
205
- }
206
-
218
+ let (entries, operations) =
219
+ merged_coordinate_entries(&snapshot.to_vec(), &journal.to_vec())?;
207
220
  self.shared_log.clear_entry_coordinates();
208
221
  self.clear_coordinate_core();
209
222
  for (_, value) in entries {
@@ -234,7 +247,7 @@ impl NativePeerbitBackbone {
234
247
  requested_replicas: usize,
235
248
  wall_time: u64,
236
249
  meta_bytes: Vec<u8>,
237
- ) -> Result<(), JsValue> {
250
+ ) -> Result<(), BackboneError> {
238
251
  let hash_number = parse_u64_string(hash_number, "coordinate hash number")?;
239
252
  let coordinates = coordinate_numbers_from_array(coordinates)?;
240
253
  self.put_coordinate_core(
@@ -258,7 +271,7 @@ impl NativePeerbitBackbone {
258
271
  delete_hashes: Array,
259
272
  wall_time: u64,
260
273
  meta_bytes: Vec<u8>,
261
- ) -> Result<(), JsValue> {
274
+ ) -> Result<(), BackboneError> {
262
275
  let row = array_from_value(coordinate_row, "coordinate plan row")?;
263
276
  let hash = string_field(&row, 0, "coordinate hash")?;
264
277
  let hash_number = stringish_field(&row, 1, "coordinate hash number")?;
@@ -301,11 +314,11 @@ impl NativePeerbitBackbone {
301
314
  true,
302
315
  );
303
316
  let profile_enabled = self.append_profile_enabled;
304
- let coordinate_delete_started = profile_enabled.then(js_sys::Date::now);
317
+ let coordinate_delete_started = profile_enabled.then(crate::time::now_ms);
305
318
  self.delete_coordinate_core_strings(next_hashes);
306
319
  self.delete_coordinate_core_strings(delete_hashes);
307
320
  if let Some(started) = coordinate_delete_started {
308
- self.append_profile.coordinate_delete_ms += js_sys::Date::now() - started;
321
+ self.append_profile.coordinate_delete_ms += crate::time::now_ms() - started;
309
322
  }
310
323
  }
311
324
 
@@ -322,7 +335,7 @@ impl NativePeerbitBackbone {
322
335
  record_journal: bool,
323
336
  ) {
324
337
  let profile_enabled = self.append_profile_enabled;
325
- let value_encode_started = profile_enabled.then(js_sys::Date::now);
338
+ let value_encode_started = profile_enabled.then(crate::time::now_ms);
326
339
  let value = encode_coordinate_value(
327
340
  &hash,
328
341
  gid,
@@ -334,24 +347,24 @@ impl NativePeerbitBackbone {
334
347
  &meta_bytes,
335
348
  );
336
349
  if let Some(started) = value_encode_started {
337
- self.append_profile.coordinate_value_encode_ms += js_sys::Date::now() - started;
350
+ self.append_profile.coordinate_value_encode_ms += crate::time::now_ms() - started;
338
351
  }
339
352
  if record_journal && self.coordinate_journal_enabled {
340
- let journal_started = profile_enabled.then(js_sys::Date::now);
353
+ let journal_started = profile_enabled.then(crate::time::now_ms);
341
354
  self.push_coordinate_journal_put(&hash, &value);
342
355
  if let Some(started) = journal_started {
343
- self.append_profile.coordinate_journal_put_ms += js_sys::Date::now() - started;
356
+ self.append_profile.coordinate_journal_put_ms += crate::time::now_ms() - started;
344
357
  }
345
358
  }
346
- let index_put_started = profile_enabled.then(js_sys::Date::now);
359
+ let index_put_started = profile_enabled.then(crate::time::now_ms);
347
360
  self.coordinate_index.insert(hash.clone());
348
361
  if let Some(started) = index_put_started {
349
- self.append_profile.coordinate_index_put_ms += js_sys::Date::now() - started;
362
+ self.append_profile.coordinate_index_put_ms += crate::time::now_ms() - started;
350
363
  }
351
- let value_put_started = profile_enabled.then(js_sys::Date::now);
364
+ let value_put_started = profile_enabled.then(crate::time::now_ms);
352
365
  self.coordinate_values.put(hash, value);
353
366
  if let Some(started) = value_put_started {
354
- self.append_profile.coordinate_value_put_ms += js_sys::Date::now() - started;
367
+ self.append_profile.coordinate_value_put_ms += crate::time::now_ms() - started;
355
368
  }
356
369
  }
357
370
 
@@ -373,7 +386,10 @@ impl NativePeerbitBackbone {
373
386
  self.coordinate_journal_record_count += 1;
374
387
  }
375
388
 
376
- pub(crate) fn delete_coordinate_core_batch(&mut self, hashes: Array) -> Result<(), JsValue> {
389
+ pub(crate) fn delete_coordinate_core_batch(
390
+ &mut self,
391
+ hashes: Array,
392
+ ) -> Result<(), BackboneError> {
377
393
  for hash in strings_from_array(hashes)? {
378
394
  self.delete_coordinate_core(&hash);
379
395
  }
@@ -389,7 +405,15 @@ impl NativePeerbitBackbone {
389
405
 
390
406
  #[cfg(test)]
391
407
  mod tests {
392
- use super::{decode_coordinate_value_core, encode_coordinate_value};
408
+ use super::{
409
+ decode_coordinate_value, decode_coordinate_value_core, encode_coordinate_value,
410
+ merged_coordinate_entries,
411
+ };
412
+ use crate::error::BackboneError;
413
+ use peerbit_indexer_core::persistence::{
414
+ encode_journal_delete_record, encode_journal_put_record, encode_journal_record,
415
+ encode_key_value_snapshot, DecodeError,
416
+ };
393
417
  use peerbit_indexer_core::wire::WireError;
394
418
 
395
419
  #[test]
@@ -419,4 +443,71 @@ mod tests {
419
443
  Err(WireError::Truncated("coordinate values"))
420
444
  ));
421
445
  }
446
+
447
+ #[test]
448
+ fn decode_coordinate_value_reports_typed_wire_errors() {
449
+ let mut bytes = encode_coordinate_value("hash", "gid", 7, &[1, 2], true, 3, 11, &[9]);
450
+ bytes.truncate(4);
451
+
452
+ let error = decode_coordinate_value(&bytes).unwrap_err();
453
+ assert_eq!(
454
+ error,
455
+ BackboneError::Wire(WireError::Truncated("coordinate hash"))
456
+ );
457
+ assert_eq!(error.to_string(), "Truncated coordinate hash");
458
+ }
459
+
460
+ #[test]
461
+ fn merges_snapshot_entries_with_journal_records() {
462
+ let snapshot = encode_key_value_snapshot(
463
+ [
464
+ ("a".to_string(), vec![1u8]),
465
+ ("b".to_string(), vec![2u8]),
466
+ ("c".to_string(), vec![3u8]),
467
+ ]
468
+ .into_iter(),
469
+ );
470
+ let mut journal = encode_journal_put_record("b", &[42]);
471
+ journal.extend_from_slice(&encode_journal_delete_record("c"));
472
+ journal.extend_from_slice(&encode_journal_put_record("d", &[4]));
473
+
474
+ let (entries, operations) = merged_coordinate_entries(&snapshot, &journal).unwrap();
475
+ assert_eq!(operations, 3);
476
+ assert_eq!(
477
+ entries,
478
+ vec![
479
+ ("a".to_string(), vec![1u8]),
480
+ ("b".to_string(), vec![42u8]),
481
+ ("d".to_string(), vec![4u8]),
482
+ ]
483
+ );
484
+
485
+ let (entries, operations) = merged_coordinate_entries(&[], &[]).unwrap();
486
+ assert_eq!(operations, 0);
487
+ assert!(entries.is_empty());
488
+ }
489
+
490
+ #[test]
491
+ fn merged_coordinate_entries_reports_typed_decode_errors() {
492
+ // The rendered message must equal the `DecodeError` Display output the
493
+ // old `decode_error` funnel flattened into an untyped string.
494
+ let snapshot = encode_key_value_snapshot([("a".to_string(), vec![1u8])].into_iter());
495
+ let truncated = &snapshot[..snapshot.len() - 1];
496
+ let expected = match super::decode_key_value_snapshot(truncated) {
497
+ Err(error) => error,
498
+ Ok(_) => panic!("snapshot decode must fail"),
499
+ };
500
+
501
+ let error = merged_coordinate_entries(truncated, &[]).unwrap_err();
502
+ assert_eq!(error, BackboneError::Decode(expected.clone()));
503
+ assert_eq!(error.to_string(), expected.to_string());
504
+
505
+ let bad_journal = encode_journal_record(&[9]);
506
+ let error = merged_coordinate_entries(&[], &bad_journal).unwrap_err();
507
+ assert_eq!(
508
+ error,
509
+ BackboneError::Decode(DecodeError::InvalidOperation(9))
510
+ );
511
+ assert_eq!(error.to_string(), "invalid operation 9");
512
+ }
422
513
  }