@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/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -14
- package/dist/src/index.js.map +1 -1
- package/dist/wasm/native_backbone.d.ts +64 -64
- package/dist/wasm/native_backbone.js +3 -0
- package/dist/wasm/native_backbone_bg.wasm +0 -0
- package/dist/wasm/native_backbone_bg.wasm.d.ts +64 -64
- package/package.json +1 -1
- package/src/append_tx/committed_latest.rs +390 -317
- package/src/append_tx/committed_no_next.rs +66 -64
- package/src/append_tx/facts.rs +213 -147
- package/src/append_tx/mod.rs +160 -58
- package/src/append_tx/storage.rs +52 -28
- package/src/coordinates.rs +135 -44
- package/src/documents.rs +340 -100
- package/src/error.rs +244 -0
- package/src/index.ts +25 -21
- package/src/js_interop.rs +230 -90
- package/src/lib.rs +4 -0
- package/src/raw_receive.rs +182 -107
- package/src/shared_log_plan.rs +65 -23
- package/src/sync_send.rs +19 -3
- package/src/time.rs +14 -0
- package/src/wire_sync.rs +147 -36
package/src/shared_log_plan.rs
CHANGED
|
@@ -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,
|
|
12
|
-
|
|
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>,
|
|
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>,
|
|
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
|
-
.
|
|
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>,
|
|
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>>,
|
|
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(
|
|
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>,
|
|
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(
|
|
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(
|
|
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>,
|
|
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>>,
|
|
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<(),
|
|
1443
|
-
self.shared_log.
|
|
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
|
|
1447
|
-
|
|
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
|
|
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
|
|
120
|
-
/// when the frame carried a raw exchange sync payload for a
|
|
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()
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
828
|
+
assert!(core
|
|
829
|
+
.try_stash(&mut buffer, data_offset, data_length)
|
|
830
|
+
.unwrap());
|
|
722
831
|
let mut buffer = Some(frame);
|
|
723
|
-
assert!(core
|
|
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
|
}
|