@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.
package/src/documents.rs CHANGED
@@ -13,12 +13,12 @@ use peerbit_log_rust::entry_v0_signature_public_key_from_storage_bytes;
13
13
  use std::collections::HashMap;
14
14
  use wasm_bindgen::prelude::*;
15
15
 
16
+ use crate::error::BackboneError;
16
17
  use crate::js_interop::{
17
18
  append_journal_delete_record, append_journal_put_record, array_strings, bytes_vec_from_array,
18
- clear_journal_prefix, decode_error, ensure_same_len, js_error, js_get, optional_string,
19
- parse_optional_u64_string, parse_u64_string, read_bytes, read_encoded_string, read_u32,
20
- read_u64, strings_from_array, write_bool, write_bytes, write_string, write_u32, write_u64,
21
- write_u8,
19
+ clear_journal_prefix, ensure_same_len, js_get, optional_string, parse_optional_u64_string,
20
+ parse_u64_string, read_bytes, read_encoded_string, read_u32, read_u64, strings_from_array,
21
+ write_bool, write_bytes, write_string, write_u32, write_u64, write_u8,
22
22
  };
23
23
  use crate::{NativePeerbitBackbone, NATIVE_BACKBONE_BYTE_EXACT_INDEX_LIMIT};
24
24
 
@@ -224,13 +224,15 @@ pub(crate) fn document_index_cached_projection_plain_put_payload_append_commit(
224
224
  })
225
225
  }
226
226
 
227
- pub(crate) fn plain_put_document_bytes_from_payload(payload_data: &[u8]) -> Result<&[u8], JsValue> {
227
+ pub(crate) fn plain_put_document_bytes_from_payload(
228
+ payload_data: &[u8],
229
+ ) -> Result<&[u8], BackboneError> {
228
230
  const PLAIN_PUT_OPERATION_PREFIX_LENGTH: usize = 6;
229
231
  if payload_data.len() < PLAIN_PUT_OPERATION_PREFIX_LENGTH {
230
- return Err(JsValue::from_str("Plain put payload is too short"));
232
+ return Err(BackboneError::PlainPutPayloadTooShort);
231
233
  }
232
234
  if payload_data[0] != 0 || payload_data[1] != 3 {
233
- return Err(JsValue::from_str("Expected native plain put payload"));
235
+ return Err(BackboneError::Expected("native plain put payload"));
234
236
  }
235
237
  let declared_len = u32::from_le_bytes([
236
238
  payload_data[2],
@@ -240,7 +242,7 @@ pub(crate) fn plain_put_document_bytes_from_payload(payload_data: &[u8]) -> Resu
240
242
  ]) as usize;
241
243
  let document_bytes = &payload_data[PLAIN_PUT_OPERATION_PREFIX_LENGTH..];
242
244
  if document_bytes.len() != declared_len {
243
- return Err(JsValue::from_str("Plain put payload length mismatch"));
245
+ return Err(BackboneError::PlainPutPayloadLengthMismatch);
244
246
  }
245
247
  Ok(document_bytes)
246
248
  }
@@ -330,12 +332,12 @@ fn encode_document_signer_fact(fact: &DocumentPreviousSignerFact) -> Vec<u8> {
330
332
  out
331
333
  }
332
334
 
333
- fn decode_document_signer_fact(bytes: &[u8]) -> Result<DocumentPreviousSignerFact, JsValue> {
335
+ fn decode_document_signer_fact(bytes: &[u8]) -> Result<DocumentPreviousSignerFact, BackboneError> {
334
336
  let mut offset = 0usize;
335
337
  let head = read_encoded_string(bytes, &mut offset, "document signer head")?;
336
338
  let public_key = read_bytes(bytes, &mut offset, "document signer public key")?;
337
339
  if offset != bytes.len() {
338
- return Err(JsValue::from_str("Trailing document signer fact bytes"));
340
+ return Err(BackboneError::TrailingDocumentSignerFactBytes);
339
341
  }
340
342
  Ok(DocumentPreviousSignerFact { head, public_key })
341
343
  }
@@ -346,7 +348,7 @@ pub(crate) fn encode_document_context_suffix(
346
348
  head: &str,
347
349
  gid: &str,
348
350
  size: u32,
349
- ) -> Result<Vec<u8>, JsValue> {
351
+ ) -> Result<Vec<u8>, BackboneError> {
350
352
  let capacity = 1usize
351
353
  .checked_add(8)
352
354
  .and_then(|value| value.checked_add(8))
@@ -355,7 +357,7 @@ pub(crate) fn encode_document_context_suffix(
355
357
  .and_then(|value| value.checked_add(4))
356
358
  .and_then(|value| value.checked_add(gid.len()))
357
359
  .and_then(|value| value.checked_add(4))
358
- .ok_or_else(|| JsValue::from_str("Document context suffix capacity overflow"))?;
360
+ .ok_or(BackboneError::DocumentContextSuffixCapacityOverflow)?;
359
361
  let mut out = Vec::with_capacity(capacity);
360
362
  // Context is @variant(0); keep this byte-for-byte aligned with Borsh.
361
363
  out.push(0);
@@ -376,24 +378,36 @@ enum ProjectionValue {
376
378
  None,
377
379
  }
378
380
 
379
- fn read_u8_projection(bytes: &[u8], offset: &mut usize, label: &str) -> Result<u8, JsValue> {
381
+ fn read_u8_projection(
382
+ bytes: &[u8],
383
+ offset: &mut usize,
384
+ label: &'static str,
385
+ ) -> Result<u8, BackboneError> {
380
386
  if *offset >= bytes.len() {
381
- return Err(JsValue::from_str(&format!("Truncated {label}")));
387
+ return Err(BackboneError::Truncated(label));
382
388
  }
383
389
  let value = bytes[*offset];
384
390
  *offset += 1;
385
391
  Ok(value)
386
392
  }
387
393
 
388
- fn read_bool_projection(bytes: &[u8], offset: &mut usize, label: &str) -> Result<bool, JsValue> {
394
+ fn read_bool_projection(
395
+ bytes: &[u8],
396
+ offset: &mut usize,
397
+ label: &'static str,
398
+ ) -> Result<bool, BackboneError> {
389
399
  match read_u8_projection(bytes, offset, label)? {
390
400
  0 => Ok(false),
391
401
  1 => Ok(true),
392
- _ => Err(JsValue::from_str(&format!("Invalid bool {label}"))),
402
+ _ => Err(BackboneError::InvalidBool(label)),
393
403
  }
394
404
  }
395
405
 
396
- fn skip_projection_value(bytes: &[u8], offset: &mut usize, kind: &str) -> Result<(), JsValue> {
406
+ fn skip_projection_value(
407
+ bytes: &[u8],
408
+ offset: &mut usize,
409
+ kind: &str,
410
+ ) -> Result<(), BackboneError> {
397
411
  match kind {
398
412
  "string" => {
399
413
  read_encoded_string(bytes, offset, "projected string")?;
@@ -419,7 +433,7 @@ fn skip_projection_value(bytes: &[u8], offset: &mut usize, kind: &str) -> Result
419
433
  if has_value == 1 {
420
434
  skip_projection_value(bytes, offset, &kind["option:".len()..])?;
421
435
  } else if has_value != 0 {
422
- return Err(JsValue::from_str("Invalid projection option marker"));
436
+ return Err(BackboneError::InvalidProjectionOptionMarker);
423
437
  }
424
438
  }
425
439
  "vec:string" => {
@@ -435,9 +449,7 @@ fn skip_projection_value(bytes: &[u8], offset: &mut usize, kind: &str) -> Result
435
449
  }
436
450
  }
437
451
  _ => {
438
- return Err(JsValue::from_str(
439
- "Unsupported document projection field type",
440
- ))
452
+ return Err(BackboneError::UnsupportedDocumentProjectionFieldType);
441
453
  }
442
454
  }
443
455
  Ok(())
@@ -447,7 +459,7 @@ fn read_projection_value(
447
459
  bytes: &[u8],
448
460
  offset: &mut usize,
449
461
  kind: &str,
450
- ) -> Result<ProjectionValue, JsValue> {
462
+ ) -> Result<ProjectionValue, BackboneError> {
451
463
  match kind {
452
464
  "string" => Ok(ProjectionValue::String(read_encoded_string(
453
465
  bytes,
@@ -486,7 +498,7 @@ fn read_projection_value(
486
498
  "projection option string",
487
499
  )?))
488
500
  } else {
489
- Err(JsValue::from_str("Invalid projection option marker"))
501
+ Err(BackboneError::InvalidProjectionOptionMarker)
490
502
  }
491
503
  }
492
504
  "option:u8" => {
@@ -498,7 +510,7 @@ fn read_projection_value(
498
510
  read_u8_projection(bytes, offset, "projection option u8")? as u64,
499
511
  ))
500
512
  } else {
501
- Err(JsValue::from_str("Invalid projection option marker"))
513
+ Err(BackboneError::InvalidProjectionOptionMarker)
502
514
  }
503
515
  }
504
516
  "option:u32" => {
@@ -510,7 +522,7 @@ fn read_projection_value(
510
522
  read_u32(bytes, offset, "projection option u32")? as u64,
511
523
  ))
512
524
  } else {
513
- Err(JsValue::from_str("Invalid projection option marker"))
525
+ Err(BackboneError::InvalidProjectionOptionMarker)
514
526
  }
515
527
  }
516
528
  "option:u64" => {
@@ -524,7 +536,7 @@ fn read_projection_value(
524
536
  "projection option u64",
525
537
  )?))
526
538
  } else {
527
- Err(JsValue::from_str("Invalid projection option marker"))
539
+ Err(BackboneError::InvalidProjectionOptionMarker)
528
540
  }
529
541
  }
530
542
  "option:bool" => {
@@ -538,7 +550,7 @@ fn read_projection_value(
538
550
  "projection option bool",
539
551
  )?))
540
552
  } else {
541
- Err(JsValue::from_str("Invalid projection option marker"))
553
+ Err(BackboneError::InvalidProjectionOptionMarker)
542
554
  }
543
555
  }
544
556
  "option:bytes" => {
@@ -552,12 +564,10 @@ fn read_projection_value(
552
564
  "projection option bytes",
553
565
  )?))
554
566
  } else {
555
- Err(JsValue::from_str("Invalid projection option marker"))
567
+ Err(BackboneError::InvalidProjectionOptionMarker)
556
568
  }
557
569
  }
558
- _ => Err(JsValue::from_str(
559
- "Unsupported projected document field type",
560
- )),
570
+ _ => Err(BackboneError::UnsupportedProjectedDocumentFieldType),
561
571
  }
562
572
  }
563
573
 
@@ -565,7 +575,7 @@ fn write_projection_value(
565
575
  out: &mut Vec<u8>,
566
576
  kind: &str,
567
577
  value: &ProjectionValue,
568
- ) -> Result<(), JsValue> {
578
+ ) -> Result<(), BackboneError> {
569
579
  match (kind, value) {
570
580
  ("string", ProjectionValue::String(value)) => write_string(out, value),
571
581
  ("u8", ProjectionValue::U64(value)) => write_u8(out, *value as u8),
@@ -604,9 +614,7 @@ fn write_projection_value(
604
614
  write_bytes(out, value);
605
615
  }
606
616
  _ => {
607
- return Err(JsValue::from_str(
608
- "Projection value does not match output type",
609
- ))
617
+ return Err(BackboneError::ProjectionValueOutputTypeMismatch);
610
618
  }
611
619
  }
612
620
  Ok(())
@@ -618,32 +626,29 @@ fn read_projected_document_fields(
618
626
  variant_value: Option<&str>,
619
627
  names: &[String],
620
628
  types: &[String],
621
- ) -> Result<HashMap<String, ProjectionValue>, JsValue> {
629
+ ) -> Result<HashMap<String, ProjectionValue>, BackboneError> {
622
630
  if names.len() != types.len() {
623
- return Err(JsValue::from_str(
624
- "Document projection plan length mismatch",
625
- ));
631
+ return Err(BackboneError::DocumentProjectionPlanLengthMismatch);
626
632
  }
627
633
  let mut offset = 0usize;
628
634
  match variant_type {
629
635
  Some("u8") => {
630
636
  let expected = variant_value
631
- .ok_or_else(|| JsValue::from_str("Missing document variant"))?
637
+ .ok_or(BackboneError::MissingDocumentVariant)?
632
638
  .parse::<u8>()
633
- .map_err(|_| JsValue::from_str("Invalid document variant"))?;
639
+ .map_err(|_| BackboneError::InvalidDocumentVariant)?;
634
640
  if read_u8_projection(encoded_document, &mut offset, "document variant")? != expected {
635
- return Err(JsValue::from_str("Document variant mismatch"));
641
+ return Err(BackboneError::DocumentVariantMismatch);
636
642
  }
637
643
  }
638
644
  Some("string") => {
639
- let expected =
640
- variant_value.ok_or_else(|| JsValue::from_str("Missing document variant"))?;
645
+ let expected = variant_value.ok_or(BackboneError::MissingDocumentVariant)?;
641
646
  if read_encoded_string(encoded_document, &mut offset, "document variant")? != expected {
642
- return Err(JsValue::from_str("Document variant mismatch"));
647
+ return Err(BackboneError::DocumentVariantMismatch);
643
648
  }
644
649
  }
645
650
  Some("") | None => {}
646
- _ => return Err(JsValue::from_str("Unsupported document variant type")),
651
+ _ => return Err(BackboneError::UnsupportedDocumentVariantType),
647
652
  }
648
653
  let mut out = HashMap::with_capacity(names.len());
649
654
  for (name, kind) in names.iter().zip(types.iter()) {
@@ -666,26 +671,26 @@ fn write_projection_variant(
666
671
  out: &mut Vec<u8>,
667
672
  variant_type: Option<&str>,
668
673
  variant_value: Option<&str>,
669
- ) -> Result<(), JsValue> {
674
+ ) -> Result<(), BackboneError> {
670
675
  match variant_type {
671
676
  Some("u8") => {
672
677
  let value = variant_value
673
- .ok_or_else(|| JsValue::from_str("Missing output variant"))?
678
+ .ok_or(BackboneError::MissingOutputVariant)?
674
679
  .parse::<u8>()
675
- .map_err(|_| JsValue::from_str("Invalid output variant"))?;
680
+ .map_err(|_| BackboneError::InvalidOutputVariant)?;
676
681
  write_u8(out, value);
677
682
  }
678
683
  Some("string") => {
679
- let value = variant_value.ok_or_else(|| JsValue::from_str("Missing output variant"))?;
684
+ let value = variant_value.ok_or(BackboneError::MissingOutputVariant)?;
680
685
  write_string(out, value);
681
686
  }
682
687
  Some("") | None => {}
683
- _ => return Err(JsValue::from_str("Unsupported output variant type")),
688
+ _ => return Err(BackboneError::UnsupportedOutputVariantType),
684
689
  }
685
690
  Ok(())
686
691
  }
687
692
 
688
- fn parse_projection_plan(plan: &JsValue) -> Result<ParsedProjectionPlan, JsValue> {
693
+ fn parse_projection_plan(plan: &JsValue) -> Result<ParsedProjectionPlan, BackboneError> {
689
694
  let document_field_names =
690
695
  array_strings(js_get(plan, "documentFieldNames"), "documentFieldNames")?;
691
696
  let document_field_types =
@@ -694,13 +699,25 @@ fn parse_projection_plan(plan: &JsValue) -> Result<ParsedProjectionPlan, JsValue
694
699
  let source_kinds = array_strings(js_get(plan, "sourceKinds"), "sourceKinds")?;
695
700
  let source_values = array_strings(js_get(plan, "sourceValues"), "sourceValues")?;
696
701
  if output_field_types.len() != source_kinds.len() || source_kinds.len() != source_values.len() {
697
- return Err(JsValue::from_str("Projection plan length mismatch"));
702
+ return Err(BackboneError::ProjectionPlanLengthMismatch);
698
703
  }
699
704
  Ok(ParsedProjectionPlan {
700
- document_variant_type: optional_string(js_get(plan, "documentVariantType")),
701
- document_variant_value: optional_string(js_get(plan, "documentVariantValue")),
702
- output_variant_type: optional_string(js_get(plan, "outputVariantType")),
703
- output_variant_value: optional_string(js_get(plan, "outputVariantValue")),
705
+ document_variant_type: optional_string(
706
+ js_get(plan, "documentVariantType"),
707
+ "documentVariantType",
708
+ )?,
709
+ document_variant_value: optional_string(
710
+ js_get(plan, "documentVariantValue"),
711
+ "documentVariantValue",
712
+ )?,
713
+ output_variant_type: optional_string(
714
+ js_get(plan, "outputVariantType"),
715
+ "outputVariantType",
716
+ )?,
717
+ output_variant_value: optional_string(
718
+ js_get(plan, "outputVariantValue"),
719
+ "outputVariantValue",
720
+ )?,
704
721
  document_field_names,
705
722
  document_field_types,
706
723
  output_field_types,
@@ -718,7 +735,7 @@ pub(crate) fn project_document_index_simple_bytes_with_plan(
718
735
  gid: &str,
719
736
  size: u32,
720
737
  signer: Option<&[u8]>,
721
- ) -> Result<Vec<u8>, JsValue> {
738
+ ) -> Result<Vec<u8>, BackboneError> {
722
739
  let document_values = read_projected_document_fields(
723
740
  encoded_document,
724
741
  plan.document_variant_type.as_deref(),
@@ -746,12 +763,12 @@ pub(crate) fn project_document_index_simple_bytes_with_plan(
746
763
  "head" => ProjectionValue::String(head.to_string()),
747
764
  "gid" => ProjectionValue::String(gid.to_string()),
748
765
  "size" => ProjectionValue::U64(size as u64),
749
- _ => return Err(JsValue::from_str("Unsupported context projection source")),
766
+ _ => return Err(BackboneError::UnsupportedContextProjectionSource),
750
767
  },
751
768
  "entryFirstSignerPublicKey" => signer
752
769
  .map(|bytes| ProjectionValue::Bytes(bytes.to_vec()))
753
770
  .unwrap_or(ProjectionValue::None),
754
- _ => return Err(JsValue::from_str("Unsupported projection source kind")),
771
+ _ => return Err(BackboneError::UnsupportedProjectionSourceKind),
755
772
  };
756
773
  write_projection_value(&mut out, &plan.output_field_types[index], &value)?;
757
774
  }
@@ -768,7 +785,7 @@ fn project_document_index_simple_bytes(
768
785
  gid: &str,
769
786
  size: u32,
770
787
  signer: JsValue,
771
- ) -> Result<Vec<u8>, JsValue> {
788
+ ) -> Result<Vec<u8>, BackboneError> {
772
789
  let plan = parse_projection_plan(plan)?;
773
790
  let created = parse_u64_string(created, "created")?;
774
791
  let modified = parse_u64_string(modified, "modified")?;
@@ -795,7 +812,7 @@ impl NativePeerbitBackbone {
795
812
  &mut self,
796
813
  schema_ir_bytes: Vec<u8>,
797
814
  ) -> Result<Array, JsValue> {
798
- let schema_ir = decode_native_schema_ir(&schema_ir_bytes).map_err(js_error)?;
815
+ let schema_ir = decode_native_schema_ir(&schema_ir_bytes).map_err(BackboneError::from)?;
799
816
  let stats = schema_ir.stats();
800
817
  self.document_schema_ir = Some(schema_ir);
801
818
  self.rebuild_document_index_from_values()?;
@@ -811,7 +828,7 @@ impl NativePeerbitBackbone {
811
828
  return Ok(());
812
829
  }
813
830
  self.document_byte_element_index_limit = limit;
814
- self.rebuild_document_index_from_values()
831
+ Ok(self.rebuild_document_index_from_values()?)
815
832
  }
816
833
 
817
834
  pub fn set_document_context_head_field(&mut self, field: u32) {
@@ -980,8 +997,8 @@ impl NativePeerbitBackbone {
980
997
  query_bytes: Vec<u8>,
981
998
  sort_bytes: Vec<u8>,
982
999
  ) -> Result<Array, JsValue> {
983
- let query = decode_query(&query_bytes).map_err(js_error)?;
984
- let sort = decode_sort(&sort_bytes).map_err(js_error)?;
1000
+ let query = decode_query(&query_bytes).map_err(BackboneError::Message)?;
1001
+ let sort = decode_sort(&sort_bytes).map_err(BackboneError::Message)?;
985
1002
  let keys = self.document_index.search(&query, &sort, None);
986
1003
  Ok(self.document_entries_for_keys(&keys))
987
1004
  }
@@ -993,8 +1010,8 @@ impl NativePeerbitBackbone {
993
1010
  offset: usize,
994
1011
  limit: usize,
995
1012
  ) -> Result<Array, JsValue> {
996
- let query = decode_query(&query_bytes).map_err(js_error)?;
997
- let sort = decode_sort(&sort_bytes).map_err(js_error)?;
1013
+ let query = decode_query(&query_bytes).map_err(BackboneError::Message)?;
1014
+ let sort = decode_sort(&sort_bytes).map_err(BackboneError::Message)?;
998
1015
  let keys = self
999
1016
  .document_index
1000
1017
  .search_page(&query, &sort, offset, Some(limit));
@@ -1002,16 +1019,16 @@ impl NativePeerbitBackbone {
1002
1019
  }
1003
1020
 
1004
1021
  pub fn document_count(&self, query_bytes: Vec<u8>) -> Result<usize, JsValue> {
1005
- let query = decode_query(&query_bytes).map_err(js_error)?;
1022
+ let query = decode_query(&query_bytes).map_err(BackboneError::Message)?;
1006
1023
  Ok(self.document_index.count(&query) as usize)
1007
1024
  }
1008
1025
 
1009
1026
  pub fn document_sum(&self, query_bytes: Vec<u8>, field: u32) -> Result<Array, JsValue> {
1010
- let query = decode_query(&query_bytes).map_err(js_error)?;
1027
+ let query = decode_query(&query_bytes).map_err(BackboneError::Message)?;
1011
1028
  let sum = self
1012
1029
  .document_index
1013
1030
  .sum(&query, FieldPath::Id(field))
1014
- .map_err(js_error)?;
1031
+ .map_err(BackboneError::Message)?;
1015
1032
  Ok(sum_to_js(sum))
1016
1033
  }
1017
1034
 
@@ -1181,12 +1198,12 @@ impl NativePeerbitBackbone {
1181
1198
  let mut entries = if snapshot.length() == 0 {
1182
1199
  Default::default()
1183
1200
  } else {
1184
- decode_key_value_snapshot(&snapshot.to_vec()).map_err(decode_error)?
1201
+ decode_key_value_snapshot(&snapshot.to_vec()).map_err(BackboneError::from)?
1185
1202
  };
1186
1203
  let journal_records = if journal.length() == 0 {
1187
1204
  Vec::new()
1188
1205
  } else {
1189
- decode_journal(&journal.to_vec()).map_err(decode_error)?
1206
+ decode_journal(&journal.to_vec()).map_err(BackboneError::from)?
1190
1207
  };
1191
1208
  let operations = journal_records.len();
1192
1209
  for record in journal_records {
@@ -1268,12 +1285,12 @@ impl NativePeerbitBackbone {
1268
1285
  let mut entries = if snapshot.length() == 0 {
1269
1286
  Default::default()
1270
1287
  } else {
1271
- decode_key_value_snapshot(&snapshot.to_vec()).map_err(decode_error)?
1288
+ decode_key_value_snapshot(&snapshot.to_vec()).map_err(BackboneError::from)?
1272
1289
  };
1273
1290
  let journal_records = if journal.length() == 0 {
1274
1291
  Vec::new()
1275
1292
  } else {
1276
- decode_journal(&journal.to_vec()).map_err(decode_error)?
1293
+ decode_journal(&journal.to_vec()).map_err(BackboneError::from)?
1277
1294
  };
1278
1295
  let operations = journal_records.len();
1279
1296
  for record in journal_records {
@@ -1322,7 +1339,7 @@ impl NativePeerbitBackbone {
1322
1339
  pub(crate) fn document_context_facts_by_key(
1323
1340
  &self,
1324
1341
  key: &str,
1325
- ) -> Result<Option<DocumentContextFacts>, JsValue> {
1342
+ ) -> Result<Option<DocumentContextFacts>, BackboneError> {
1326
1343
  let Some(document_fields) = self.document_index.document_fields_by_id(key) else {
1327
1344
  return Ok(None);
1328
1345
  };
@@ -1332,21 +1349,21 @@ impl NativePeerbitBackbone {
1332
1349
  fn document_context_facts_from_fields(
1333
1350
  &self,
1334
1351
  document_fields: &DocumentFields,
1335
- ) -> Result<Option<DocumentContextFacts>, JsValue> {
1352
+ ) -> Result<Option<DocumentContextFacts>, BackboneError> {
1336
1353
  let Some(fields) = self.document_context_fields else {
1337
1354
  return Ok(None);
1338
1355
  };
1339
1356
  let created = document_u64_field(document_fields, fields.created)
1340
- .ok_or_else(|| JsValue::from_str("Missing document context created field"))?;
1357
+ .ok_or(BackboneError::MissingDocumentContextField("created"))?;
1341
1358
  let modified = document_u64_field(document_fields, fields.modified)
1342
- .ok_or_else(|| JsValue::from_str("Missing document context modified field"))?;
1359
+ .ok_or(BackboneError::MissingDocumentContextField("modified"))?;
1343
1360
  let head = document_string_field(document_fields, fields.head)
1344
- .ok_or_else(|| JsValue::from_str("Missing document context head field"))?;
1361
+ .ok_or(BackboneError::MissingDocumentContextField("head"))?;
1345
1362
  let gid = document_string_field(document_fields, fields.gid)
1346
- .ok_or_else(|| JsValue::from_str("Missing document context gid field"))?;
1363
+ .ok_or(BackboneError::MissingDocumentContextField("gid"))?;
1347
1364
  let size = document_u64_field(document_fields, fields.size)
1348
1365
  .and_then(|value| u32::try_from(value).ok())
1349
- .ok_or_else(|| JsValue::from_str("Missing document context size field"))?;
1366
+ .ok_or(BackboneError::MissingDocumentContextField("size"))?;
1350
1367
  Ok(Some(DocumentContextFacts {
1351
1368
  created,
1352
1369
  modified,
@@ -1390,7 +1407,7 @@ impl NativePeerbitBackbone {
1390
1407
  new_head: Option<&str>,
1391
1408
  previous_head: Option<&str>,
1392
1409
  record_document_journal: bool,
1393
- ) -> Result<(), JsValue> {
1410
+ ) -> Result<(), BackboneError> {
1394
1411
  let prepared = self.prepare_document_encoded_parts_put(
1395
1412
  key,
1396
1413
  value_prefix_bytes,
@@ -1416,31 +1433,31 @@ impl NativePeerbitBackbone {
1416
1433
  new_head: Option<&str>,
1417
1434
  previous_head: Option<&str>,
1418
1435
  record_document_journal: bool,
1419
- ) -> Result<PreparedDocumentEncodedPartsPut, JsValue> {
1436
+ ) -> Result<PreparedDocumentEncodedPartsPut, BackboneError> {
1420
1437
  self.document_byte_element_index_limit = byte_element_index_limit;
1421
1438
  let profile_enabled = self.append_profile_enabled;
1422
- let extract_started = profile_enabled.then(js_sys::Date::now);
1439
+ let extract_started = profile_enabled.then(crate::time::now_ms);
1423
1440
  let fields = {
1424
- let schema_ir = self.document_schema_ir.as_ref().ok_or_else(|| {
1425
- js_error("Native backbone document schema IR has not been configured")
1426
- })?;
1441
+ let schema_ir = self
1442
+ .document_schema_ir
1443
+ .as_ref()
1444
+ .ok_or(BackboneError::DocumentSchemaIrNotConfigured)?;
1427
1445
  extract_encoded_document_fields_from_parts_with_byte_limits(
1428
1446
  &schema_ir,
1429
1447
  &value_prefix_bytes,
1430
1448
  &value_suffix_bytes,
1431
1449
  byte_element_index_limit,
1432
1450
  NATIVE_BACKBONE_BYTE_EXACT_INDEX_LIMIT,
1433
- )
1434
- .map_err(js_error)?
1451
+ )?
1435
1452
  };
1436
1453
  if let Some(started) = extract_started {
1437
- self.append_profile.document_index_extract_ms += js_sys::Date::now() - started;
1454
+ self.append_profile.document_index_extract_ms += crate::time::now_ms() - started;
1438
1455
  }
1439
- let value_build_started = profile_enabled.then(js_sys::Date::now);
1456
+ let value_build_started = profile_enabled.then(crate::time::now_ms);
1440
1457
  value_prefix_bytes.reserve(value_suffix_bytes.len());
1441
1458
  value_prefix_bytes.extend_from_slice(&value_suffix_bytes);
1442
1459
  if let Some(started) = value_build_started {
1443
- self.append_profile.document_index_value_build_ms += js_sys::Date::now() - started;
1460
+ self.append_profile.document_index_value_build_ms += crate::time::now_ms() - started;
1444
1461
  }
1445
1462
  Ok(PreparedDocumentEncodedPartsPut {
1446
1463
  key,
@@ -1472,7 +1489,7 @@ impl NativePeerbitBackbone {
1472
1489
  if should_record_document_journal {
1473
1490
  self.push_document_journal_put(&key, &value_bytes);
1474
1491
  }
1475
- let value_put_started = profile_enabled.then(js_sys::Date::now);
1492
+ let value_put_started = profile_enabled.then(crate::time::now_ms);
1476
1493
  let was_existing = if known_existing {
1477
1494
  self.document_values.put(key.clone(), value_bytes);
1478
1495
  true
@@ -1482,16 +1499,16 @@ impl NativePeerbitBackbone {
1482
1499
  .is_some()
1483
1500
  };
1484
1501
  if let Some(started) = value_put_started {
1485
- self.append_profile.document_value_put_ms += js_sys::Date::now() - started;
1502
+ self.append_profile.document_value_put_ms += crate::time::now_ms() - started;
1486
1503
  }
1487
- let index_put_started = profile_enabled.then(js_sys::Date::now);
1504
+ let index_put_started = profile_enabled.then(crate::time::now_ms);
1488
1505
  if known_existing || was_existing {
1489
1506
  self.document_index.put(&key, fields);
1490
1507
  } else {
1491
1508
  self.document_index.put_new_unchecked(&key, fields);
1492
1509
  }
1493
1510
  if let Some(started) = index_put_started {
1494
- self.append_profile.document_index_put_ms += js_sys::Date::now() - started;
1511
+ self.append_profile.document_index_put_ms += crate::time::now_ms() - started;
1495
1512
  }
1496
1513
  self.update_document_head_key(
1497
1514
  &key,
@@ -1526,7 +1543,7 @@ impl NativePeerbitBackbone {
1526
1543
  .insert(new_head.to_string(), key.to_string());
1527
1544
  }
1528
1545
 
1529
- fn refresh_document_previous_signer_fact(&mut self, key: &str) -> Result<(), JsValue> {
1546
+ fn refresh_document_previous_signer_fact(&mut self, key: &str) -> Result<(), BackboneError> {
1530
1547
  let Some(context) = self.document_context_facts_by_key(key)? else {
1531
1548
  self.delete_document_previous_signer_fact(key, true);
1532
1549
  return Ok(());
@@ -1578,7 +1595,7 @@ impl NativePeerbitBackbone {
1578
1595
  self.document_signer_journal_record_count = 0;
1579
1596
  }
1580
1597
 
1581
- fn rebuild_document_index_from_values(&mut self) -> Result<(), JsValue> {
1598
+ fn rebuild_document_index_from_values(&mut self) -> Result<(), BackboneError> {
1582
1599
  let Some(schema_ir) = self.document_schema_ir.clone() else {
1583
1600
  self.document_index.clear();
1584
1601
  self.document_key_by_head.clear();
@@ -1600,8 +1617,7 @@ impl NativePeerbitBackbone {
1600
1617
  &[],
1601
1618
  self.document_byte_element_index_limit,
1602
1619
  NATIVE_BACKBONE_BYTE_EXACT_INDEX_LIMIT,
1603
- )
1604
- .map_err(js_error)?;
1620
+ )?;
1605
1621
  if let Some(context) = self.document_context_facts_from_fields(&fields)? {
1606
1622
  self.document_key_by_head.insert(context.head, key.clone());
1607
1623
  }
@@ -1696,3 +1712,227 @@ impl NativePeerbitBackbone {
1696
1712
  self.document_key_by_head.reserve(batch_len);
1697
1713
  }
1698
1714
  }
1715
+
1716
+ #[cfg(test)]
1717
+ mod tests {
1718
+ use super::*;
1719
+
1720
+ fn projection_plan(
1721
+ source_kinds: &[&str],
1722
+ source_values: &[&str],
1723
+ output_field_types: &[&str],
1724
+ ) -> ParsedProjectionPlan {
1725
+ ParsedProjectionPlan {
1726
+ document_variant_type: None,
1727
+ document_variant_value: None,
1728
+ output_variant_type: None,
1729
+ output_variant_value: None,
1730
+ document_field_names: Vec::new(),
1731
+ document_field_types: Vec::new(),
1732
+ output_field_types: output_field_types.iter().map(|s| s.to_string()).collect(),
1733
+ source_kinds: source_kinds.iter().map(|s| s.to_string()).collect(),
1734
+ source_values: source_values.iter().map(|s| s.to_string()).collect(),
1735
+ }
1736
+ }
1737
+
1738
+ #[test]
1739
+ fn plain_put_payload_reports_typed_errors() {
1740
+ let error = plain_put_document_bytes_from_payload(&[0, 3]).unwrap_err();
1741
+ assert_eq!(error, BackboneError::PlainPutPayloadTooShort);
1742
+ assert_eq!(error.to_string(), "Plain put payload is too short");
1743
+
1744
+ let error = plain_put_document_bytes_from_payload(&[1, 3, 0, 0, 0, 0]).unwrap_err();
1745
+ assert_eq!(error, BackboneError::Expected("native plain put payload"));
1746
+ assert_eq!(error.to_string(), "Expected native plain put payload");
1747
+
1748
+ let error = plain_put_document_bytes_from_payload(&[0, 3, 2, 0, 0, 0, 9]).unwrap_err();
1749
+ assert_eq!(error, BackboneError::PlainPutPayloadLengthMismatch);
1750
+ assert_eq!(error.to_string(), "Plain put payload length mismatch");
1751
+ }
1752
+
1753
+ #[test]
1754
+ fn document_signer_fact_decode_reports_typed_errors() {
1755
+ let fact = DocumentPreviousSignerFact {
1756
+ head: "head".to_string(),
1757
+ public_key: vec![1, 2, 3],
1758
+ };
1759
+ let encoded = encode_document_signer_fact(&fact);
1760
+ let decoded = decode_document_signer_fact(&encoded).unwrap();
1761
+ assert_eq!(decoded.head, fact.head);
1762
+ assert_eq!(decoded.public_key, fact.public_key);
1763
+
1764
+ let mut trailing = encoded.clone();
1765
+ trailing.push(0);
1766
+ let error = match decode_document_signer_fact(&trailing) {
1767
+ Ok(_) => panic!("expected trailing-bytes error"),
1768
+ Err(error) => error,
1769
+ };
1770
+ assert_eq!(error, BackboneError::TrailingDocumentSignerFactBytes);
1771
+ assert_eq!(error.to_string(), "Trailing document signer fact bytes");
1772
+ }
1773
+
1774
+ #[test]
1775
+ fn projection_plan_length_mismatch_renders_historical_string() {
1776
+ assert_eq!(
1777
+ BackboneError::ProjectionPlanLengthMismatch.to_string(),
1778
+ "Projection plan length mismatch"
1779
+ );
1780
+ }
1781
+
1782
+ #[test]
1783
+ fn query_decode_errors_forward_the_indexer_string_verbatim() {
1784
+ // decode_query/decode_sort have no typed core error; the document
1785
+ // query/count/sum boundaries forward their String verbatim through
1786
+ // BackboneError::Message. Assert the rendered message is byte-for-byte
1787
+ // the string the indexer core produced (matching the old js_error
1788
+ // funnel that also built Message(error.to_string())).
1789
+ let raw = decode_query(&[0xff]).unwrap_err();
1790
+ assert_eq!(BackboneError::Message(raw.clone()).to_string(), raw);
1791
+ }
1792
+
1793
+ #[test]
1794
+ fn projected_document_fields_report_typed_errors() {
1795
+ let error =
1796
+ read_projected_document_fields(&[], None, None, &["a".to_string()], &[]).unwrap_err();
1797
+ assert_eq!(error, BackboneError::DocumentProjectionPlanLengthMismatch);
1798
+ assert_eq!(
1799
+ error.to_string(),
1800
+ "Document projection plan length mismatch"
1801
+ );
1802
+
1803
+ let error =
1804
+ read_projected_document_fields(&[], Some("u16"), Some("1"), &[], &[]).unwrap_err();
1805
+ assert_eq!(error, BackboneError::UnsupportedDocumentVariantType);
1806
+ assert_eq!(error.to_string(), "Unsupported document variant type");
1807
+
1808
+ let error = read_projected_document_fields(&[], Some("u8"), None, &[], &[]).unwrap_err();
1809
+ assert_eq!(error, BackboneError::MissingDocumentVariant);
1810
+ assert_eq!(error.to_string(), "Missing document variant");
1811
+
1812
+ let error =
1813
+ read_projected_document_fields(&[], Some("u8"), Some("abc"), &[], &[]).unwrap_err();
1814
+ assert_eq!(error, BackboneError::InvalidDocumentVariant);
1815
+ assert_eq!(error.to_string(), "Invalid document variant");
1816
+
1817
+ let error =
1818
+ read_projected_document_fields(&[], Some("u8"), Some("1"), &[], &[]).unwrap_err();
1819
+ assert_eq!(error, BackboneError::Truncated("document variant"));
1820
+ assert_eq!(error.to_string(), "Truncated document variant");
1821
+
1822
+ let error =
1823
+ read_projected_document_fields(&[0], Some("u8"), Some("1"), &[], &[]).unwrap_err();
1824
+ assert_eq!(error, BackboneError::DocumentVariantMismatch);
1825
+ assert_eq!(error.to_string(), "Document variant mismatch");
1826
+ }
1827
+
1828
+ #[test]
1829
+ fn projection_variant_and_value_writers_report_typed_errors() {
1830
+ let mut out = Vec::new();
1831
+ let error = write_projection_variant(&mut out, Some("u8"), None).unwrap_err();
1832
+ assert_eq!(error, BackboneError::MissingOutputVariant);
1833
+ assert_eq!(error.to_string(), "Missing output variant");
1834
+
1835
+ let error = write_projection_variant(&mut out, Some("u8"), Some("abc")).unwrap_err();
1836
+ assert_eq!(error, BackboneError::InvalidOutputVariant);
1837
+ assert_eq!(error.to_string(), "Invalid output variant");
1838
+
1839
+ let error = write_projection_variant(&mut out, Some("u16"), Some("1")).unwrap_err();
1840
+ assert_eq!(error, BackboneError::UnsupportedOutputVariantType);
1841
+ assert_eq!(error.to_string(), "Unsupported output variant type");
1842
+
1843
+ let error = write_projection_value(&mut out, "string", &ProjectionValue::None).unwrap_err();
1844
+ assert_eq!(error, BackboneError::ProjectionValueOutputTypeMismatch);
1845
+ assert_eq!(
1846
+ error.to_string(),
1847
+ "Projection value does not match output type"
1848
+ );
1849
+ }
1850
+
1851
+ #[test]
1852
+ fn projection_value_readers_report_typed_errors() {
1853
+ let mut offset = 0usize;
1854
+ let error = read_projection_value(&[2], &mut offset, "bool").unwrap_err();
1855
+ assert_eq!(error, BackboneError::InvalidBool("projection bool"));
1856
+ assert_eq!(error.to_string(), "Invalid bool projection bool");
1857
+
1858
+ let mut offset = 0usize;
1859
+ let error = read_projection_value(&[2], &mut offset, "option:u8").unwrap_err();
1860
+ assert_eq!(error, BackboneError::InvalidProjectionOptionMarker);
1861
+ assert_eq!(error.to_string(), "Invalid projection option marker");
1862
+
1863
+ let mut offset = 0usize;
1864
+ let error = read_projection_value(&[], &mut offset, "u128").unwrap_err();
1865
+ assert_eq!(error, BackboneError::UnsupportedProjectedDocumentFieldType);
1866
+ assert_eq!(
1867
+ error.to_string(),
1868
+ "Unsupported projected document field type"
1869
+ );
1870
+
1871
+ let mut offset = 0usize;
1872
+ let error = skip_projection_value(&[], &mut offset, "u128").unwrap_err();
1873
+ assert_eq!(error, BackboneError::UnsupportedDocumentProjectionFieldType);
1874
+ assert_eq!(
1875
+ error.to_string(),
1876
+ "Unsupported document projection field type"
1877
+ );
1878
+ }
1879
+
1880
+ #[test]
1881
+ fn projection_sources_report_typed_errors() {
1882
+ let plan = projection_plan(&["context"], &["bogus"], &["u64"]);
1883
+ let error =
1884
+ project_document_index_simple_bytes_with_plan(&[], &plan, 0, 0, "h", "g", 0, None)
1885
+ .unwrap_err();
1886
+ assert_eq!(error, BackboneError::UnsupportedContextProjectionSource);
1887
+ assert_eq!(error.to_string(), "Unsupported context projection source");
1888
+
1889
+ let plan = projection_plan(&["bogus"], &["x"], &["u64"]);
1890
+ let error =
1891
+ project_document_index_simple_bytes_with_plan(&[], &plan, 0, 0, "h", "g", 0, None)
1892
+ .unwrap_err();
1893
+ assert_eq!(error, BackboneError::UnsupportedProjectionSourceKind);
1894
+ assert_eq!(error.to_string(), "Unsupported projection source kind");
1895
+ }
1896
+
1897
+ #[test]
1898
+ fn document_context_field_variants_render_exact_messages() {
1899
+ for label in ["created", "modified", "head", "gid", "size"] {
1900
+ assert_eq!(
1901
+ BackboneError::MissingDocumentContextField(label).to_string(),
1902
+ format!("Missing document context {label} field")
1903
+ );
1904
+ }
1905
+ assert_eq!(
1906
+ BackboneError::MissingDocumentContextField("created").to_string(),
1907
+ "Missing document context created field"
1908
+ );
1909
+ }
1910
+
1911
+ #[test]
1912
+ fn document_index_put_chain_variants_render_exact_messages() {
1913
+ for (error, message) in [
1914
+ (
1915
+ BackboneError::DocumentContextSuffixCapacityOverflow,
1916
+ "Document context suffix capacity overflow",
1917
+ ),
1918
+ (
1919
+ BackboneError::DocumentSchemaIrNotConfigured,
1920
+ "Native backbone document schema IR has not been configured",
1921
+ ),
1922
+ (
1923
+ BackboneError::MissingCachedDocumentProjectionPlan,
1924
+ "Missing cached document projection plan",
1925
+ ),
1926
+ (
1927
+ BackboneError::MissingPlainPutPayloadForDocumentIndex,
1928
+ "Missing plain put payload for document index",
1929
+ ),
1930
+ (
1931
+ BackboneError::MissingPlainPutPayloadForDocumentProjection,
1932
+ "Missing plain put payload for document projection",
1933
+ ),
1934
+ ] {
1935
+ assert_eq!(error.to_string(), message);
1936
+ }
1937
+ }
1938
+ }