@peerbit/native-backbone 0.1.2 → 0.1.4

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.
@@ -7,9 +7,10 @@ use wasm_bindgen::prelude::*;
7
7
  use crate::coordinates::{
8
8
  coordinate_core_value_to_row, decode_coordinate_value, CoordinateCoreValue,
9
9
  };
10
+ use crate::error::BackboneError;
10
11
  use crate::js_interop::{
11
- array_from_value, ensure_same_len, number_strings_to_array, parse_u64_string,
12
- string_batches_from_array, strings_from_array, strings_to_array, usize_values_from_array,
12
+ array_from_value, ensure_same_len, parse_u64_string, string_batches_from_array,
13
+ strings_from_array, strings_to_array, usize_values_from_array,
13
14
  };
14
15
  use crate::NativePeerbitBackbone;
15
16
 
@@ -46,7 +47,7 @@ pub(crate) fn coordinate_commits_from_string_columns(
46
47
  next_hash_batches: Array,
47
48
  assigned_to_range_boundaries: Uint8Array,
48
49
  requested_replicas: Array,
49
- ) -> Result<Vec<EntryCoordinateCommit>, JsValue> {
50
+ ) -> Result<Vec<EntryCoordinateCommit>, BackboneError> {
50
51
  let hashes = strings_from_array(hashes)?;
51
52
  let gids = strings_from_array(gids)?;
52
53
  let hash_numbers = strings_from_array(hash_numbers)?;
@@ -78,7 +79,7 @@ pub(crate) fn coordinate_commits_from_u64_columns(
78
79
  next_hash_batches: Array,
79
80
  assigned_to_range_boundaries: Uint8Array,
80
81
  requested_replicas: Uint32Array,
81
- ) -> Result<Vec<EntryCoordinateCommit>, JsValue> {
82
+ ) -> Result<Vec<EntryCoordinateCommit>, BackboneError> {
82
83
  let hashes = strings_from_array(hashes)?;
83
84
  let gids = strings_from_array(gids)?;
84
85
  let hash_numbers = hash_numbers.to_vec();
@@ -99,7 +100,7 @@ pub(crate) fn coordinate_commits_from_u64_columns(
99
100
  let coordinate_total = coordinate_counts
100
101
  .iter()
101
102
  .try_fold(0usize, |sum, count| sum.checked_add(*count as usize))
102
- .ok_or_else(|| JsValue::from_str("Coordinate count overflow"))?;
103
+ .ok_or(BackboneError::CoordinateCountOverflow)?;
103
104
  ensure_same_len(
104
105
  coordinate_total,
105
106
  coordinates.len(),
@@ -132,7 +133,7 @@ fn coordinate_commits_from_parts(
132
133
  next_hash_batches: Vec<Vec<String>>,
133
134
  assigned_to_range_boundaries: Uint8Array,
134
135
  requested_replicas: Vec<usize>,
135
- ) -> Result<Vec<EntryCoordinateCommit>, JsValue> {
136
+ ) -> Result<Vec<EntryCoordinateCommit>, BackboneError> {
136
137
  ensure_same_len(hashes.len(), gids.len(), "coordinate commit gid")?;
137
138
  ensure_same_len(
138
139
  hashes.len(),
@@ -182,28 +183,31 @@ fn coordinate_commits_from_parts(
182
183
  Ok(commits)
183
184
  }
184
185
 
185
- fn coordinate_batches_from_array(values: Array) -> Result<Vec<Vec<u64>>, JsValue> {
186
+ fn coordinate_batches_from_array(values: Array) -> Result<Vec<Vec<u64>>, BackboneError> {
186
187
  let mut out = Vec::with_capacity(values.length() as usize);
187
188
  for index in 0..values.length() {
188
189
  let value = values.get(index);
189
190
  if !Array::is_array(&value) {
190
- return Err(JsValue::from_str("Expected coordinate batch array"));
191
+ return Err(BackboneError::ExpectedArray("coordinate batch"));
191
192
  }
192
193
  out.push(coordinate_numbers_from_array(Array::from(&value))?);
193
194
  }
194
195
  Ok(out)
195
196
  }
196
197
 
197
- pub(crate) fn coordinate_numbers_from_array(values: Array) -> Result<Vec<u64>, JsValue> {
198
+ pub(crate) fn coordinate_numbers_from_array(values: Array) -> Result<Vec<u64>, BackboneError> {
198
199
  let mut out = Vec::with_capacity(values.length() as usize);
199
200
  for index in 0..values.length() {
200
201
  let value = values.get(index);
201
202
  if let Some(value) = value.as_string() {
202
203
  out.push(parse_u64_string(&value, "coordinate")?);
203
204
  } else if let Some(value) = value.as_f64() {
204
- out.push(value as u64);
205
+ out.push(
206
+ crate::js_interop::checked_u64_from_f64(value)
207
+ .ok_or(BackboneError::Expected("coordinate string array"))?,
208
+ );
205
209
  } else {
206
- return Err(JsValue::from_str("Expected coordinate string array"));
210
+ return Err(BackboneError::Expected("coordinate string array"));
207
211
  }
208
212
  }
209
213
  Ok(out)
@@ -409,7 +413,8 @@ impl NativePeerbitBackbone {
409
413
  requested_replicas,
410
414
  0,
411
415
  Vec::new(),
412
- )
416
+ )?;
417
+ Ok(())
413
418
  }
414
419
 
415
420
  pub fn delete_entry_coordinates(&mut self, hash: &str) -> bool {
@@ -421,7 +426,8 @@ impl NativePeerbitBackbone {
421
426
  pub fn delete_entry_coordinates_batch(&mut self, hashes: Array) -> Result<(), JsValue> {
422
427
  let hashes_for_core = hashes.clone();
423
428
  self.shared_log.delete_entry_coordinates_batch(hashes)?;
424
- self.delete_coordinate_core_batch(hashes_for_core)
429
+ self.delete_coordinate_core_batch(hashes_for_core)?;
430
+ Ok(())
425
431
  }
426
432
 
427
433
  pub fn commit_entry_coordinates(
@@ -458,7 +464,8 @@ impl NativePeerbitBackbone {
458
464
  0,
459
465
  Vec::new(),
460
466
  )?;
461
- self.delete_coordinate_core_batch(next_hashes_for_core)
467
+ self.delete_coordinate_core_batch(next_hashes_for_core)?;
468
+ Ok(())
462
469
  }
463
470
 
464
471
  pub fn commit_entry_coordinates_batch(
@@ -1330,7 +1337,7 @@ impl NativePeerbitBackbone {
1330
1337
  include_self: bool,
1331
1338
  full_replica_fallback: bool,
1332
1339
  include_strict_full_replica: bool,
1333
- ) -> Result<Option<bool>, JsValue> {
1340
+ ) -> Result<Option<bool>, BackboneError> {
1334
1341
  if hashes.is_empty() {
1335
1342
  return Ok(Some(false));
1336
1343
  }
@@ -1360,7 +1367,7 @@ impl NativePeerbitBackbone {
1360
1367
  let Some(common_replicas) = common_replicas else {
1361
1368
  return Ok(Some(false));
1362
1369
  };
1363
- self.shared_log.full_replica_self_leader_for_replicas(
1370
+ Ok(self.shared_log.full_replica_self_leader_for_replicas(
1364
1371
  common_replicas,
1365
1372
  role_age_ms,
1366
1373
  now,
@@ -1370,7 +1377,7 @@ impl NativePeerbitBackbone {
1370
1377
  include_self,
1371
1378
  full_replica_fallback,
1372
1379
  include_strict_full_replica,
1373
- )
1380
+ )?)
1374
1381
  }
1375
1382
 
1376
1383
  #[allow(clippy::too_many_arguments)]
@@ -1386,7 +1393,7 @@ impl NativePeerbitBackbone {
1386
1393
  include_self: bool,
1387
1394
  full_replica_fallback: bool,
1388
1395
  include_strict_full_replica: bool,
1389
- ) -> Result<Option<Vec<String>>, JsValue> {
1396
+ ) -> Result<Option<Vec<String>>, BackboneError> {
1390
1397
  let empty = || Ok(None);
1391
1398
  if hashes.is_empty() {
1392
1399
  return empty();
@@ -1439,15 +1446,15 @@ impl NativePeerbitBackbone {
1439
1446
  &mut self,
1440
1447
  coordinate: CoordinateCoreValue,
1441
1448
  record_journal: bool,
1442
- ) -> Result<(), JsValue> {
1443
- self.shared_log.put_entry_coordinates(
1449
+ ) -> Result<(), BackboneError> {
1450
+ self.shared_log.put_entry_coordinates_core(
1444
1451
  coordinate.hash.clone(),
1445
1452
  coordinate.gid.clone(),
1446
- coordinate.hash_number.to_string(),
1447
- number_strings_to_array(&coordinate.coordinates),
1453
+ coordinate.hash_number,
1454
+ coordinate.coordinates.clone(),
1448
1455
  coordinate.assigned_to_range_boundary,
1449
1456
  coordinate.requested_replicas,
1450
- )?;
1457
+ );
1451
1458
  self.put_coordinate_core(
1452
1459
  coordinate.hash,
1453
1460
  &coordinate.gid,
@@ -1462,3 +1469,38 @@ impl NativePeerbitBackbone {
1462
1469
  Ok(())
1463
1470
  }
1464
1471
  }
1472
+
1473
+ #[cfg(test)]
1474
+ mod tests {
1475
+ use crate::error::BackboneError;
1476
+
1477
+ #[test]
1478
+ fn coordinate_array_error_message_matches_historical_string() {
1479
+ // `coordinate_numbers_from_array` needs a live JS engine, but its
1480
+ // error variant must keep rendering the exact string previously
1481
+ // built with `JsValue::from_str`.
1482
+ assert_eq!(
1483
+ BackboneError::Expected("coordinate string array").to_string(),
1484
+ "Expected coordinate string array"
1485
+ );
1486
+ }
1487
+
1488
+ #[test]
1489
+ fn plan_error_messages_match_historical_strings() {
1490
+ assert_eq!(
1491
+ BackboneError::CoordinateCountOverflow.to_string(),
1492
+ "Coordinate count overflow"
1493
+ );
1494
+ assert_eq!(
1495
+ BackboneError::ExpectedArray("coordinate batch").to_string(),
1496
+ "Expected coordinate batch array"
1497
+ );
1498
+ }
1499
+
1500
+ #[test]
1501
+ fn shared_log_errors_render_through_the_bridge_unchanged() {
1502
+ let inner = peerbit_shared_log_rust::SharedLogError::MissingCompactAppendFacts;
1503
+ let expected = inner.to_string();
1504
+ assert_eq!(BackboneError::SharedLog(inner).to_string(), expected);
1505
+ }
1506
+ }
package/src/sync_send.rs CHANGED
@@ -16,6 +16,7 @@ use peerbit_log_rust::NativeLogBlockStore;
16
16
  use peerbit_wire::sync_payload::{encode_raw_exchange_sync_payload_refs, SyncPayloadHeadRef};
17
17
  use wasm_bindgen::prelude::*;
18
18
 
19
+ use crate::error::BackboneError;
19
20
  use crate::js_interop::{ensure_same_len, string_batches_from_array, strings_from_array};
20
21
  use crate::NativePeerbitBackbone;
21
22
 
@@ -55,6 +56,12 @@ pub(crate) fn encode_sync_payload_from_store(
55
56
  ))
56
57
  }
57
58
 
59
+ pub(crate) fn reserved_bytes(reserved: &[u8]) -> Result<[u8; 4], BackboneError> {
60
+ reserved
61
+ .try_into()
62
+ .map_err(|_| BackboneError::ExpectedReservedBytes)
63
+ }
64
+
58
65
  pub(crate) fn block_byte_lengths_core(blocks: &NativeLogBlockStore, hashes: &[String]) -> Vec<u32> {
59
66
  hashes
60
67
  .iter()
@@ -94,9 +101,7 @@ impl NativePeerbitBackbone {
94
101
  let hashes = strings_from_array(hashes)?;
95
102
  let gid_refrences = string_batches_from_array(gid_refrences, "sync send gid references")?;
96
103
  ensure_same_len(hashes.len(), gid_refrences.len(), "sync send heads")?;
97
- let reserved: [u8; 4] = reserved
98
- .try_into()
99
- .map_err(|_| JsValue::from_str("expected 4 reserved bytes"))?;
104
+ let reserved = reserved_bytes(reserved)?;
100
105
  match encode_sync_payload_from_store(
101
106
  &self.blocks,
102
107
  topic,
@@ -164,6 +169,17 @@ mod tests {
164
169
  assert_eq!(parsed.heads.len(), 2);
165
170
  }
166
171
 
172
+ #[test]
173
+ fn reserved_bytes_require_exactly_four() {
174
+ assert_eq!(reserved_bytes(&[1, 2, 3, 4]).unwrap(), [1, 2, 3, 4]);
175
+
176
+ for bytes in [&[][..], &[1, 2, 3][..], &[1, 2, 3, 4, 5][..]] {
177
+ let error = reserved_bytes(bytes).unwrap_err();
178
+ assert_eq!(error, BackboneError::ExpectedReservedBytes);
179
+ assert_eq!(error.to_string(), "expected 4 reserved bytes");
180
+ }
181
+ }
182
+
167
183
  #[test]
168
184
  fn missing_blocks_fall_back() {
169
185
  let store = store_with(&[("zb2AA", vec![0xde])]);
package/src/time.rs ADDED
@@ -0,0 +1,14 @@
1
+ #[cfg(target_arch = "wasm32")]
2
+ #[inline]
3
+ pub(crate) fn now_ms() -> f64 {
4
+ js_sys::Date::now()
5
+ }
6
+
7
+ #[cfg(not(target_arch = "wasm32"))]
8
+ #[inline]
9
+ pub(crate) fn now_ms() -> f64 {
10
+ std::time::SystemTime::now()
11
+ .duration_since(std::time::UNIX_EPOCH)
12
+ .map(|duration| duration.as_secs_f64() * 1000.0)
13
+ .unwrap_or(0.0)
14
+ }
package/src/wire_sync.rs CHANGED
@@ -29,6 +29,7 @@ use peerbit_wire::{record_to_words, RECORD_FLAG_SYNC_STASHED, RECORD_WORDS};
29
29
  use std::collections::{HashMap, VecDeque};
30
30
  use wasm_bindgen::prelude::*;
31
31
 
32
+ use crate::error::BackboneError;
32
33
  use crate::js_interop::strings_slice_to_array;
33
34
  use crate::NativePeerbitBackbone;
34
35
 
@@ -116,46 +117,48 @@ impl WireSyncCore {
116
117
  }
117
118
  }
118
119
 
119
- /// Try to stash a decoded-and-verified DataMessage frame. Returns `true`
120
- /// when the frame carried a raw exchange sync payload for a registered
121
- /// topic addressed to this node.
120
+ /// Try to stash a decoded-and-verified DataMessage frame. Returns
121
+ /// `Ok(true)` when the frame carried a raw exchange sync payload for a
122
+ /// registered topic addressed to this node. The only error is the
123
+ /// (unreachable-by-construction) invariant breach of the frame buffer
124
+ /// disappearing between the checks and the take — previously a panic.
122
125
  pub(crate) fn try_stash(
123
126
  &mut self,
124
127
  frame: &mut Option<Vec<u8>>,
125
128
  data_offset: usize,
126
129
  data_length: usize,
127
- ) -> bool {
130
+ ) -> Result<bool, BackboneError> {
128
131
  if self.topic_refs.is_empty() {
129
- return false;
132
+ return Ok(false);
130
133
  }
131
134
  let Some(frame_bytes) = frame.as_deref() else {
132
- return false;
135
+ return Ok(false);
133
136
  };
134
137
  let Some(payload) = frame_bytes.get(data_offset..data_offset + data_length) else {
135
- return false;
138
+ return Ok(false);
136
139
  };
137
140
  let Ok(pubsub) = parse_pubsub_data(payload) else {
138
- return false;
141
+ return Ok(false);
139
142
  };
140
143
  if !pubsub
141
144
  .topics
142
145
  .iter()
143
146
  .any(|topic| self.topic_refs.contains_key(topic))
144
147
  {
145
- return false;
148
+ return Ok(false);
146
149
  }
147
150
  let Some(data) = payload.get(pubsub.data_offset..pubsub.data_offset + pubsub.data_length)
148
151
  else {
149
- return false;
152
+ return Ok(false);
150
153
  };
151
154
  let Ok(parsed) = parse_raw_exchange_rpc_request(data) else {
152
- return false;
155
+ return Ok(false);
153
156
  };
154
157
  let Ok(meta) = decode_frame_delivery_meta(frame_bytes) else {
155
- return false;
158
+ return Ok(false);
156
159
  };
157
160
  if meta.variant != VARIANT_DATA || !self.delivered_locally(meta.mode.as_ref()) {
158
- return false;
161
+ return Ok(false);
159
162
  }
160
163
 
161
164
  if self
@@ -168,7 +171,7 @@ impl WireSyncCore {
168
171
  // byte-identical. Keep the pinned entry (replacing it would reset
169
172
  // the pin) and report the frame as stashed.
170
173
  self.counters.stashed += 1;
171
- return true;
174
+ return Ok(true);
172
175
  }
173
176
 
174
177
  let heads = parsed
@@ -180,7 +183,9 @@ impl WireSyncCore {
180
183
  ..head
181
184
  })
182
185
  .collect();
183
- let frame = frame.take().expect("frame checked above");
186
+ let Some(frame) = frame.take() else {
187
+ return Err(BackboneError::WireSyncStashFrameTaken);
188
+ };
184
189
  let frame_length = frame.len();
185
190
  if let Some(previous) = self.stash.insert(
186
191
  meta.id,
@@ -209,7 +214,7 @@ impl WireSyncCore {
209
214
  self.counters.evicted += 1;
210
215
  }
211
216
  }
212
- true
217
+ Ok(true)
213
218
  }
214
219
 
215
220
  pub(crate) fn get(&self, id: &[u8]) -> Option<&StashedSyncMessage> {
@@ -237,6 +242,24 @@ impl WireSyncCore {
237
242
  }
238
243
  }
239
244
 
245
+ /// Pin a stashed entry and return it for meta extraction. `Ok(None)`
246
+ /// means the id is not stashed; the error is the
247
+ /// (unreachable-by-construction) invariant breach of a just-pinned entry
248
+ /// missing from the stash — previously a panic.
249
+ pub(crate) fn pin_and_get(
250
+ &mut self,
251
+ id: &[u8],
252
+ ) -> Result<Option<&StashedSyncMessage>, BackboneError> {
253
+ if !self.pin(id) {
254
+ return Ok(None);
255
+ }
256
+ self.counters.meta_reads += 1;
257
+ match self.get(id) {
258
+ Some(stashed) => Ok(Some(stashed)),
259
+ None => Err(BackboneError::WireSyncPinnedEntryMissing),
260
+ }
261
+ }
262
+
240
263
  pub(crate) fn release(&mut self, id: &[u8]) -> bool {
241
264
  let Ok(id) = <&[u8; ID_LENGTH]>::try_from(id) else {
242
265
  return false;
@@ -341,11 +364,25 @@ impl NativeWireSyncSession {
341
364
  let mut words = Vec::with_capacity(records.len() * RECORD_WORDS);
342
365
  for (record, buffer) in records.iter().zip(buffers.iter_mut()) {
343
366
  let stashed = record_is_stash_candidate(record)
344
- && self.core.try_stash(
367
+ && match self.core.try_stash(
345
368
  buffer,
346
369
  record.data_offset as usize,
347
370
  record.data_length as usize,
348
- );
371
+ ) {
372
+ Ok(stashed) => stashed,
373
+ // The wasm ABI of this hot-path function is frozen (it
374
+ // must stay a plain Uint32Array return), so the typed
375
+ // error is thrown instead of returned as a Result. NOTE:
376
+ // throw_val unwinds without dropping the exported-method
377
+ // borrow guard, so the session object would be unusable
378
+ // afterwards ("recursive use of an object" on every
379
+ // later call). Acceptable only because this path is
380
+ // unreachable by construction (the frame is checked Some
381
+ // above and nothing takes it in between) — the
382
+ // pre-refactor expect() trapped the whole wasm instance
383
+ // here instead.
384
+ Err(error) => wasm_bindgen::throw_val(error.into()),
385
+ };
349
386
  record_to_words(record, &mut words);
350
387
  if stashed {
351
388
  let flag_word = words.len() - RECORD_WORDS;
@@ -361,11 +398,18 @@ impl NativeWireSyncSession {
361
398
  /// fallback anymore, so the entry must survive FIFO eviction until
362
399
  /// `release` is called when processing finishes.
363
400
  pub fn stashed_meta(&mut self, id: &[u8]) -> JsValue {
364
- if !self.core.pin(id) {
365
- return JsValue::UNDEFINED;
366
- }
367
- self.core.counters.meta_reads += 1;
368
- let stashed = self.core.get(id).expect("pinned above");
401
+ let stashed = match self.core.pin_and_get(id) {
402
+ Ok(Some(stashed)) => stashed,
403
+ Ok(None) => return JsValue::UNDEFINED,
404
+ // The wasm ABI is frozen (`undefined` is a valid success value,
405
+ // so errors cannot travel as a Result); throw the typed error.
406
+ // NOTE: throw_val leaks the exported-method borrow guard,
407
+ // leaving the session object permanently unusable — acceptable
408
+ // only because pin_and_get's Err path is unreachable by
409
+ // construction (pin() returning true implies the same-id get()
410
+ // succeeds).
411
+ Err(error) => wasm_bindgen::throw_val(error.into()),
412
+ };
369
413
  let hashes = Array::new();
370
414
  let gid_refrences = Array::new();
371
415
  let mut byte_lengths: Vec<u32> = Vec::with_capacity(stashed.heads.len());
@@ -566,7 +610,9 @@ mod tests {
566
610
  core.register_topic("topic".to_string());
567
611
  let (frame, data_offset, data_length) = sync_frame(7, "topic", silent_to_self(), &heads());
568
612
  let mut buffer = Some(frame.clone());
569
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
613
+ assert!(core
614
+ .try_stash(&mut buffer, data_offset, data_length)
615
+ .unwrap());
570
616
  assert!(buffer.is_none(), "stash takes frame ownership");
571
617
  assert_eq!(core.stash_len(), 1);
572
618
 
@@ -592,7 +638,9 @@ mod tests {
592
638
  let (frame, data_offset, data_length) =
593
639
  sync_frame(1, "other-topic", silent_to_self(), &heads());
594
640
  let mut buffer = Some(frame);
595
- assert!(!core.try_stash(&mut buffer, data_offset, data_length));
641
+ assert!(!core
642
+ .try_stash(&mut buffer, data_offset, data_length)
643
+ .unwrap());
596
644
  assert!(buffer.is_some(), "rejected frames keep their buffer");
597
645
 
598
646
  let relay_mode = Some(DeliveryMode::Silent {
@@ -601,17 +649,23 @@ mod tests {
601
649
  });
602
650
  let (frame, data_offset, data_length) = sync_frame(2, "topic", relay_mode, &heads());
603
651
  let mut buffer = Some(frame);
604
- assert!(!core.try_stash(&mut buffer, data_offset, data_length));
652
+ assert!(!core
653
+ .try_stash(&mut buffer, data_offset, data_length)
654
+ .unwrap());
605
655
 
606
656
  let (frame, data_offset, data_length) =
607
657
  sync_frame(3, "topic", Some(DeliveryMode::AnyWhere), &heads());
608
658
  let mut buffer = Some(frame);
609
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
659
+ assert!(core
660
+ .try_stash(&mut buffer, data_offset, data_length)
661
+ .unwrap());
610
662
 
611
663
  core.unregister_topic("topic");
612
664
  let (frame, data_offset, data_length) = sync_frame(4, "topic", silent_to_self(), &heads());
613
665
  let mut buffer = Some(frame);
614
- assert!(!core.try_stash(&mut buffer, data_offset, data_length));
666
+ assert!(!core
667
+ .try_stash(&mut buffer, data_offset, data_length)
668
+ .unwrap());
615
669
  }
616
670
 
617
671
  #[test]
@@ -637,7 +691,9 @@ mod tests {
637
691
  let mut frame = frame;
638
692
  frame[2] = (index >> 8) as u8; // second byte of the 32-byte id
639
693
  let mut buffer = Some(frame);
640
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
694
+ assert!(core
695
+ .try_stash(&mut buffer, data_offset, data_length)
696
+ .unwrap());
641
697
  }
642
698
  assert_eq!(core.stash_len(), WIRE_SYNC_MAX_STASHED_MESSAGES);
643
699
  assert_eq!(core.counters.evicted, 3);
@@ -654,7 +710,9 @@ mod tests {
654
710
  core.register_topic("topic".to_string());
655
711
  let (frame, data_offset, data_length) = sync_frame(0, "topic", silent_to_self(), &heads());
656
712
  let mut buffer = Some(frame);
657
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
713
+ assert!(core
714
+ .try_stash(&mut buffer, data_offset, data_length)
715
+ .unwrap());
658
716
  assert!(core.pin(&[0u8; ID_LENGTH]));
659
717
  assert!(!core.pin(&[9u8; ID_LENGTH]), "missing ids cannot be pinned");
660
718
 
@@ -665,7 +723,9 @@ mod tests {
665
723
  let mut frame = frame;
666
724
  frame[2] = (index >> 8) as u8; // second byte of the 32-byte id
667
725
  let mut buffer = Some(frame);
668
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
726
+ assert!(core
727
+ .try_stash(&mut buffer, data_offset, data_length)
728
+ .unwrap());
669
729
  }
670
730
  assert!(core.counters.evicted > 0);
671
731
  // Pinned entry survives (the cap counts it, so the stash holds the
@@ -687,11 +747,15 @@ mod tests {
687
747
  core.register_topic("topic".to_string());
688
748
  let (frame, data_offset, data_length) = sync_frame(5, "topic", silent_to_self(), &heads());
689
749
  let mut buffer = Some(frame.clone());
690
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
750
+ assert!(core
751
+ .try_stash(&mut buffer, data_offset, data_length)
752
+ .unwrap());
691
753
  assert!(core.pin(&[5u8; ID_LENGTH]));
692
754
 
693
755
  let mut buffer = Some(frame);
694
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
756
+ assert!(core
757
+ .try_stash(&mut buffer, data_offset, data_length)
758
+ .unwrap());
695
759
  assert!(
696
760
  buffer.is_some(),
697
761
  "duplicates of pinned entries keep their buffer"
@@ -706,21 +770,68 @@ mod tests {
706
770
  frame[2] = 0xff; // distinct id space from the pinned entry
707
771
  frame[3] = (index >> 8) as u8;
708
772
  let mut buffer = Some(frame);
709
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
773
+ assert!(core
774
+ .try_stash(&mut buffer, data_offset, data_length)
775
+ .unwrap());
710
776
  }
711
777
  assert!(core.blocks(&[5u8; ID_LENGTH], None).is_some());
712
778
  assert!(core.release(&[5u8; ID_LENGTH]));
713
779
  }
714
780
 
781
+ #[test]
782
+ fn pin_and_get_resolves_stashed_entries_and_counts_meta_reads() {
783
+ let mut core = WireSyncCore::new("self-hash".to_string());
784
+ core.register_topic("topic".to_string());
785
+ let (frame, data_offset, data_length) = sync_frame(4, "topic", silent_to_self(), &heads());
786
+ let mut buffer = Some(frame);
787
+ assert!(core
788
+ .try_stash(&mut buffer, data_offset, data_length)
789
+ .unwrap());
790
+
791
+ assert!(
792
+ core.pin_and_get(&[3u8; ID_LENGTH]).unwrap().is_none(),
793
+ "unstashed ids resolve to None"
794
+ );
795
+ assert_eq!(
796
+ core.pin_and_get(&[4u8; ID_LENGTH])
797
+ .unwrap()
798
+ .unwrap()
799
+ .head_count(),
800
+ 2
801
+ );
802
+ assert_eq!(core.counters.meta_reads, 1);
803
+ assert!(core.release(&[4u8; ID_LENGTH]));
804
+ }
805
+
806
+ #[test]
807
+ fn wire_sync_invariant_errors_render_their_messages() {
808
+ // Both invariant breaches replace former `expect` panics and cannot
809
+ // be reached through the public API (a `None` frame short-circuits
810
+ // `try_stash` early, and `pin` only succeeds for present entries);
811
+ // pin down the strings a breach would surface to JS.
812
+ assert_eq!(
813
+ BackboneError::WireSyncStashFrameTaken.to_string(),
814
+ "wire sync stash frame already taken"
815
+ );
816
+ assert_eq!(
817
+ BackboneError::WireSyncPinnedEntryMissing.to_string(),
818
+ "wire sync pinned stash entry missing"
819
+ );
820
+ }
821
+
715
822
  #[test]
716
823
  fn restashing_same_id_replaces_entry() {
717
824
  let mut core = WireSyncCore::new("self-hash".to_string());
718
825
  core.register_topic("topic".to_string());
719
826
  let (frame, data_offset, data_length) = sync_frame(9, "topic", silent_to_self(), &heads());
720
827
  let mut buffer = Some(frame.clone());
721
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
828
+ assert!(core
829
+ .try_stash(&mut buffer, data_offset, data_length)
830
+ .unwrap());
722
831
  let mut buffer = Some(frame);
723
- assert!(core.try_stash(&mut buffer, data_offset, data_length));
832
+ assert!(core
833
+ .try_stash(&mut buffer, data_offset, data_length)
834
+ .unwrap());
724
835
  assert_eq!(core.stash_len(), 1);
725
836
  assert_eq!(core.counters.stashed, 2);
726
837
  }