@peerbit/native-backbone 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +27 -0
- package/dist/src/benchmark.d.ts +19 -0
- package/dist/src/benchmark.d.ts.map +1 -0
- package/dist/src/benchmark.js +12 -0
- package/dist/src/benchmark.js.map +1 -0
- package/dist/src/index.d.ts +1561 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3406 -0
- package/dist/src/index.js.map +1 -0
- package/dist/wasm/README.md +27 -0
- package/dist/wasm/native_backbone.d.ts +1609 -0
- package/dist/wasm/native_backbone.js +10221 -0
- package/dist/wasm/native_backbone_bg.wasm +0 -0
- package/dist/wasm/native_backbone_bg.wasm.d.ts +625 -0
- package/package.json +64 -0
- package/src/append_tx/committed_latest.rs +2063 -0
- package/src/append_tx/committed_no_next.rs +1165 -0
- package/src/append_tx/facts.rs +791 -0
- package/src/append_tx/mod.rs +751 -0
- package/src/append_tx/storage.rs +599 -0
- package/src/benchmark.ts +58 -0
- package/src/coordinates.rs +422 -0
- package/src/documents.rs +1698 -0
- package/src/graph_blocks.rs +362 -0
- package/src/index.ts +9134 -0
- package/src/js_interop.rs +448 -0
- package/src/lib.rs +144 -0
- package/src/profile.rs +164 -0
- package/src/raw_receive.rs +1724 -0
- package/src/shared_log_plan.rs +1464 -0
- package/src/sync_send.rs +180 -0
- package/src/wire_sync.rs +727 -0
package/src/wire_sync.rs
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
//! Receive fusion for shared-log raw exchange-heads sync.
|
|
2
|
+
//!
|
|
3
|
+
//! The `peerbit_wire` envelope codec is compiled into this wasm module (rather
|
|
4
|
+
//! than the standalone `@peerbit/network-rust` module) so the single boundary
|
|
5
|
+
//! write that `decode_and_verify_batch` already performs — copying the inbound
|
|
6
|
+
//! frame into wasm linear memory — can be shared with the native-backbone
|
|
7
|
+
//! prepared-raw-receive pipeline. Frames whose DataMessage payload is a
|
|
8
|
+
//! shared-log RPC `RawExchangeHeadsMessage([0,7])` on a registered topic are
|
|
9
|
+
//! kept ("stashed") in that memory; the entry block bytes are later fed into
|
|
10
|
+
//! `prepare_raw_receive_*` as in-memory slices instead of a second
|
|
11
|
+
//! JS→wasm copy, and JS never materializes the borsh `RawEntryWithRefs`.
|
|
12
|
+
//!
|
|
13
|
+
//! One [`NativeWireSyncSession`] belongs to one node: its DirectStream feeds
|
|
14
|
+
//! `decode_and_verify_batch` and its shared-log programs register their topics
|
|
15
|
+
//! and consume stashed payloads by message id.
|
|
16
|
+
//!
|
|
17
|
+
//! The stash core is `JsValue`-free so host `cargo test` covers the decision
|
|
18
|
+
//! and eviction logic.
|
|
19
|
+
|
|
20
|
+
use js_sys::{Array, Uint32Array, Uint8Array};
|
|
21
|
+
use peerbit_wire::sync_payload::{
|
|
22
|
+
parse_pubsub_data, parse_raw_exchange_rpc_request, SyncPayloadHead,
|
|
23
|
+
};
|
|
24
|
+
use peerbit_wire::wire::{
|
|
25
|
+
decode_and_verify_frames, decode_frame_delivery_meta, DeliveryMode, FrameRecord, VerifyStatus,
|
|
26
|
+
ID_LENGTH, VARIANT_DATA,
|
|
27
|
+
};
|
|
28
|
+
use peerbit_wire::{record_to_words, RECORD_FLAG_SYNC_STASHED, RECORD_WORDS};
|
|
29
|
+
use std::collections::{HashMap, VecDeque};
|
|
30
|
+
use wasm_bindgen::prelude::*;
|
|
31
|
+
|
|
32
|
+
use crate::js_interop::strings_slice_to_array;
|
|
33
|
+
use crate::NativePeerbitBackbone;
|
|
34
|
+
|
|
35
|
+
/// Bounds for never-consumed stash entries (a message can be stashed at the
|
|
36
|
+
/// wire level and then dropped before program dispatch, e.g. by the seen
|
|
37
|
+
/// cache). FIFO-evicted; eviction of a never-resolved entry only costs the
|
|
38
|
+
/// fused fast path — the TS fallback still processes the message. Entries
|
|
39
|
+
/// resolved for processing (`stashed_meta`) are pinned and excluded from
|
|
40
|
+
/// eviction until `release`: after resolve there is no TS fallback (the RPC
|
|
41
|
+
/// layer skipped the borsh decode), so evicting a pinned entry would drop the
|
|
42
|
+
/// message's heads. Pinned entries are bounded by the message-processing
|
|
43
|
+
/// concurrency of their consumers, not by these caps.
|
|
44
|
+
pub(crate) const WIRE_SYNC_MAX_STASHED_MESSAGES: usize = 512;
|
|
45
|
+
pub(crate) const WIRE_SYNC_MAX_STASHED_BYTES: usize = 64 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
pub(crate) struct StashedSyncMessage {
|
|
48
|
+
frame: Vec<u8>,
|
|
49
|
+
/// Head byte offsets are absolute within `frame`.
|
|
50
|
+
heads: Vec<SyncPayloadHead>,
|
|
51
|
+
reserved: [u8; 4],
|
|
52
|
+
payload_length: usize,
|
|
53
|
+
/// Pinned entries are mid-processing (resolved via `stashed_meta`, not
|
|
54
|
+
/// yet released); they are excluded from FIFO eviction.
|
|
55
|
+
pinned: bool,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
|
|
59
|
+
pub(crate) struct WireSyncCounters {
|
|
60
|
+
pub stashed: u32,
|
|
61
|
+
pub evicted: u32,
|
|
62
|
+
pub meta_reads: u32,
|
|
63
|
+
pub block_copy_outs: u32,
|
|
64
|
+
pub released: u32,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[derive(Default)]
|
|
68
|
+
pub(crate) struct WireSyncCore {
|
|
69
|
+
self_hash: String,
|
|
70
|
+
topic_refs: HashMap<String, usize>,
|
|
71
|
+
stash: HashMap<[u8; ID_LENGTH], StashedSyncMessage>,
|
|
72
|
+
order: VecDeque<[u8; ID_LENGTH]>,
|
|
73
|
+
stashed_bytes: usize,
|
|
74
|
+
pub(crate) counters: WireSyncCounters,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl WireSyncCore {
|
|
78
|
+
pub(crate) fn new(self_hash: String) -> Self {
|
|
79
|
+
WireSyncCore {
|
|
80
|
+
self_hash,
|
|
81
|
+
..WireSyncCore::default()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub(crate) fn register_topic(&mut self, topic: String) {
|
|
86
|
+
*self.topic_refs.entry(topic).or_insert(0) += 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(crate) fn unregister_topic(&mut self, topic: &str) -> bool {
|
|
90
|
+
match self.topic_refs.get_mut(topic) {
|
|
91
|
+
Some(count) if *count > 1 => {
|
|
92
|
+
*count -= 1;
|
|
93
|
+
true
|
|
94
|
+
}
|
|
95
|
+
Some(_) => {
|
|
96
|
+
self.topic_refs.remove(topic);
|
|
97
|
+
true
|
|
98
|
+
}
|
|
99
|
+
None => false,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub(crate) fn topic_count(&self) -> usize {
|
|
104
|
+
self.topic_refs.len()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn delivered_locally(&self, mode: Option<&DeliveryMode>) -> bool {
|
|
108
|
+
// Mirrors TopicControlPlane.onDataMessage: explicit receivers must
|
|
109
|
+
// include this node; AnyWhere modes are always delivered locally.
|
|
110
|
+
match mode {
|
|
111
|
+
Some(DeliveryMode::Silent { to, .. }) | Some(DeliveryMode::Acknowledge { to, .. }) => {
|
|
112
|
+
to.iter().any(|hash| hash == &self.self_hash)
|
|
113
|
+
}
|
|
114
|
+
Some(DeliveryMode::AnyWhere) | Some(DeliveryMode::AcknowledgeAnyWhere { .. }) => true,
|
|
115
|
+
Some(DeliveryMode::Traced { .. }) | None => false,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
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.
|
|
122
|
+
pub(crate) fn try_stash(
|
|
123
|
+
&mut self,
|
|
124
|
+
frame: &mut Option<Vec<u8>>,
|
|
125
|
+
data_offset: usize,
|
|
126
|
+
data_length: usize,
|
|
127
|
+
) -> bool {
|
|
128
|
+
if self.topic_refs.is_empty() {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
let Some(frame_bytes) = frame.as_deref() else {
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
let Some(payload) = frame_bytes.get(data_offset..data_offset + data_length) else {
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
137
|
+
let Ok(pubsub) = parse_pubsub_data(payload) else {
|
|
138
|
+
return false;
|
|
139
|
+
};
|
|
140
|
+
if !pubsub
|
|
141
|
+
.topics
|
|
142
|
+
.iter()
|
|
143
|
+
.any(|topic| self.topic_refs.contains_key(topic))
|
|
144
|
+
{
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
let Some(data) = payload.get(pubsub.data_offset..pubsub.data_offset + pubsub.data_length)
|
|
148
|
+
else {
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
let Ok(parsed) = parse_raw_exchange_rpc_request(data) else {
|
|
152
|
+
return false;
|
|
153
|
+
};
|
|
154
|
+
let Ok(meta) = decode_frame_delivery_meta(frame_bytes) else {
|
|
155
|
+
return false;
|
|
156
|
+
};
|
|
157
|
+
if meta.variant != VARIANT_DATA || !self.delivered_locally(meta.mode.as_ref()) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if self
|
|
162
|
+
.stash
|
|
163
|
+
.get(&meta.id)
|
|
164
|
+
.is_some_and(|existing| existing.pinned)
|
|
165
|
+
{
|
|
166
|
+
// Duplicate delivery of a message that is mid-processing: the id
|
|
167
|
+
// is part of the signed header, so the stashed frame is
|
|
168
|
+
// byte-identical. Keep the pinned entry (replacing it would reset
|
|
169
|
+
// the pin) and report the frame as stashed.
|
|
170
|
+
self.counters.stashed += 1;
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let heads = parsed
|
|
175
|
+
.heads
|
|
176
|
+
.into_iter()
|
|
177
|
+
.map(|head| SyncPayloadHead {
|
|
178
|
+
// Translate payload-relative offsets to frame-absolute ones.
|
|
179
|
+
bytes_offset: data_offset + pubsub.data_offset + head.bytes_offset,
|
|
180
|
+
..head
|
|
181
|
+
})
|
|
182
|
+
.collect();
|
|
183
|
+
let frame = frame.take().expect("frame checked above");
|
|
184
|
+
let frame_length = frame.len();
|
|
185
|
+
if let Some(previous) = self.stash.insert(
|
|
186
|
+
meta.id,
|
|
187
|
+
StashedSyncMessage {
|
|
188
|
+
frame,
|
|
189
|
+
heads,
|
|
190
|
+
reserved: parsed.reserved,
|
|
191
|
+
payload_length: data_length,
|
|
192
|
+
pinned: false,
|
|
193
|
+
},
|
|
194
|
+
) {
|
|
195
|
+
self.stashed_bytes -= previous.frame.len();
|
|
196
|
+
self.order.retain(|id| id != &meta.id);
|
|
197
|
+
}
|
|
198
|
+
self.stashed_bytes += frame_length;
|
|
199
|
+
self.order.push_back(meta.id);
|
|
200
|
+
self.counters.stashed += 1;
|
|
201
|
+
while self.stash.len() > WIRE_SYNC_MAX_STASHED_MESSAGES
|
|
202
|
+
|| self.stashed_bytes > WIRE_SYNC_MAX_STASHED_BYTES
|
|
203
|
+
{
|
|
204
|
+
let Some(oldest) = self.order.pop_front() else {
|
|
205
|
+
break;
|
|
206
|
+
};
|
|
207
|
+
if let Some(evicted) = self.stash.remove(&oldest) {
|
|
208
|
+
self.stashed_bytes -= evicted.frame.len();
|
|
209
|
+
self.counters.evicted += 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pub(crate) fn get(&self, id: &[u8]) -> Option<&StashedSyncMessage> {
|
|
216
|
+
let id: &[u8; ID_LENGTH] = id.try_into().ok()?;
|
|
217
|
+
self.stash.get(id)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Pin a stashed entry while it is being processed: pinned entries are
|
|
221
|
+
/// taken out of the eviction order until `release` removes them, so the
|
|
222
|
+
/// block bytes an in-flight `StashBackedRawExchangeHeadsMessage` refers
|
|
223
|
+
/// to cannot be dropped by later `try_stash` calls. Returns `false` when
|
|
224
|
+
/// the id is not stashed.
|
|
225
|
+
pub(crate) fn pin(&mut self, id: &[u8]) -> bool {
|
|
226
|
+
let Ok(id) = <&[u8; ID_LENGTH]>::try_from(id) else {
|
|
227
|
+
return false;
|
|
228
|
+
};
|
|
229
|
+
match self.stash.get_mut(id) {
|
|
230
|
+
Some(entry) if entry.pinned => true,
|
|
231
|
+
Some(entry) => {
|
|
232
|
+
entry.pinned = true;
|
|
233
|
+
self.order.retain(|entry_id| entry_id != id);
|
|
234
|
+
true
|
|
235
|
+
}
|
|
236
|
+
None => false,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pub(crate) fn release(&mut self, id: &[u8]) -> bool {
|
|
241
|
+
let Ok(id) = <&[u8; ID_LENGTH]>::try_from(id) else {
|
|
242
|
+
return false;
|
|
243
|
+
};
|
|
244
|
+
if let Some(removed) = self.stash.remove(id) {
|
|
245
|
+
self.stashed_bytes -= removed.frame.len();
|
|
246
|
+
self.order.retain(|entry| entry != id);
|
|
247
|
+
self.counters.released += 1;
|
|
248
|
+
true
|
|
249
|
+
} else {
|
|
250
|
+
false
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
pub(crate) fn stash_len(&self) -> usize {
|
|
255
|
+
self.stash.len()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Copy the selected head block bytes out of a stashed message. `indexes`
|
|
259
|
+
/// of `None` selects every head. This is the in-wasm handoff used by the
|
|
260
|
+
/// stashed prepare entry points and — as `Uint8Array`s — the JS fallback.
|
|
261
|
+
pub(crate) fn blocks(&self, id: &[u8], indexes: Option<&[u32]>) -> Option<Vec<Vec<u8>>> {
|
|
262
|
+
let stashed = self.get(id)?;
|
|
263
|
+
let select = |head: &SyncPayloadHead| {
|
|
264
|
+
stashed
|
|
265
|
+
.frame
|
|
266
|
+
.get(head.bytes_offset..head.bytes_offset + head.bytes_length)
|
|
267
|
+
.map(<[u8]>::to_vec)
|
|
268
|
+
};
|
|
269
|
+
match indexes {
|
|
270
|
+
Some(indexes) => indexes
|
|
271
|
+
.iter()
|
|
272
|
+
.map(|index| stashed.heads.get(*index as usize).and_then(select))
|
|
273
|
+
.collect(),
|
|
274
|
+
None => stashed.heads.iter().map(select).collect(),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[cfg(test)]
|
|
280
|
+
impl StashedSyncMessage {
|
|
281
|
+
pub(crate) fn head_count(&self) -> usize {
|
|
282
|
+
self.heads.len()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
pub(crate) fn head(&self, index: usize) -> Option<&SyncPayloadHead> {
|
|
286
|
+
self.heads.get(index)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pub(crate) fn reserved(&self) -> [u8; 4] {
|
|
290
|
+
self.reserved
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Per-node receive-fusion state: the fused wire decoder for DirectStream and
|
|
295
|
+
/// the stash consumed by shared-log programs. See the module docs.
|
|
296
|
+
#[wasm_bindgen]
|
|
297
|
+
pub struct NativeWireSyncSession {
|
|
298
|
+
pub(crate) core: WireSyncCore,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[wasm_bindgen]
|
|
302
|
+
impl NativeWireSyncSession {
|
|
303
|
+
#[wasm_bindgen(constructor)]
|
|
304
|
+
pub fn new(self_hash: String) -> Self {
|
|
305
|
+
NativeWireSyncSession {
|
|
306
|
+
core: WireSyncCore::new(self_hash),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
pub fn register_topic(&mut self, topic: String) {
|
|
311
|
+
self.core.register_topic(topic);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
pub fn unregister_topic(&mut self, topic: &str) -> bool {
|
|
315
|
+
self.core.unregister_topic(topic)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
pub fn topic_count(&self) -> usize {
|
|
319
|
+
self.core.topic_count()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Drop-in replacement for `peerbit_wire`'s `decode_and_verify_batch`
|
|
323
|
+
/// (same flat u32 record layout) that additionally stashes raw exchange
|
|
324
|
+
/// sync payloads for registered topics, flagging their records with
|
|
325
|
+
/// `RECORD_FLAG_SYNC_STASHED`.
|
|
326
|
+
pub fn decode_and_verify_batch(&mut self, frames: Array, now_ms: f64) -> Vec<u32> {
|
|
327
|
+
let mut buffers: Vec<Option<Vec<u8>>> = frames
|
|
328
|
+
.iter()
|
|
329
|
+
.map(|value| {
|
|
330
|
+
value
|
|
331
|
+
.dyn_into::<Uint8Array>()
|
|
332
|
+
.ok()
|
|
333
|
+
.map(|array| array.to_vec())
|
|
334
|
+
})
|
|
335
|
+
.collect();
|
|
336
|
+
let slices: Vec<&[u8]> = buffers
|
|
337
|
+
.iter()
|
|
338
|
+
.map(|buffer| buffer.as_deref().unwrap_or(&[]))
|
|
339
|
+
.collect();
|
|
340
|
+
let records = decode_and_verify_frames(&slices, now_ms as u64);
|
|
341
|
+
let mut words = Vec::with_capacity(records.len() * RECORD_WORDS);
|
|
342
|
+
for (record, buffer) in records.iter().zip(buffers.iter_mut()) {
|
|
343
|
+
let stashed = record_is_stash_candidate(record)
|
|
344
|
+
&& self.core.try_stash(
|
|
345
|
+
buffer,
|
|
346
|
+
record.data_offset as usize,
|
|
347
|
+
record.data_length as usize,
|
|
348
|
+
);
|
|
349
|
+
record_to_words(record, &mut words);
|
|
350
|
+
if stashed {
|
|
351
|
+
let flag_word = words.len() - RECORD_WORDS;
|
|
352
|
+
words[flag_word] |= RECORD_FLAG_SYNC_STASHED;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
words
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Stash facts for a message id: `[hashes, gidRefrences, byteLengths,
|
|
359
|
+
/// reserved, payloadLength]`, or `undefined` when not stashed. Does not
|
|
360
|
+
/// consume the entry, but pins it: a resolved message has no TS decode
|
|
361
|
+
/// fallback anymore, so the entry must survive FIFO eviction until
|
|
362
|
+
/// `release` is called when processing finishes.
|
|
363
|
+
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");
|
|
369
|
+
let hashes = Array::new();
|
|
370
|
+
let gid_refrences = Array::new();
|
|
371
|
+
let mut byte_lengths: Vec<u32> = Vec::with_capacity(stashed.heads.len());
|
|
372
|
+
for head in &stashed.heads {
|
|
373
|
+
hashes.push(&JsValue::from_str(&head.hash));
|
|
374
|
+
gid_refrences.push(&strings_slice_to_array(&head.gid_refrences));
|
|
375
|
+
byte_lengths.push(head.bytes_length as u32);
|
|
376
|
+
}
|
|
377
|
+
let out = Array::new();
|
|
378
|
+
out.push(&hashes);
|
|
379
|
+
out.push(&gid_refrences);
|
|
380
|
+
out.push(&Uint32Array::from(byte_lengths.as_slice()));
|
|
381
|
+
out.push(&Uint8Array::from(stashed.reserved.as_slice()));
|
|
382
|
+
out.push(&JsValue::from_f64(stashed.payload_length as f64));
|
|
383
|
+
out.into()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// Copy head block bytes out to JS (fallback paths only — the fused path
|
|
387
|
+
/// hands blocks to `prepare_stashed_raw_receive_*` inside wasm memory).
|
|
388
|
+
pub fn stashed_blocks(&mut self, id: &[u8], indexes: Option<Uint32Array>) -> JsValue {
|
|
389
|
+
let indexes = indexes.map(|indexes| indexes.to_vec());
|
|
390
|
+
let Some(blocks) = self.core.blocks(id, indexes.as_deref()) else {
|
|
391
|
+
return JsValue::UNDEFINED;
|
|
392
|
+
};
|
|
393
|
+
self.core.counters.block_copy_outs += blocks.len() as u32;
|
|
394
|
+
let out = Array::new();
|
|
395
|
+
for block in blocks {
|
|
396
|
+
out.push(&Uint8Array::from(block.as_slice()));
|
|
397
|
+
}
|
|
398
|
+
out.into()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
pub fn release(&mut self, id: &[u8]) -> bool {
|
|
402
|
+
self.core.release(id)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
pub fn stash_len(&self) -> usize {
|
|
406
|
+
self.core.stash_len()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// `[stashed, evicted, metaReads, blockCopyOuts, released]`.
|
|
410
|
+
pub fn counters(&self) -> Vec<u32> {
|
|
411
|
+
let counters = &self.core.counters;
|
|
412
|
+
vec![
|
|
413
|
+
counters.stashed,
|
|
414
|
+
counters.evicted,
|
|
415
|
+
counters.meta_reads,
|
|
416
|
+
counters.block_copy_outs,
|
|
417
|
+
counters.released,
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn record_is_stash_candidate(record: &FrameRecord) -> bool {
|
|
423
|
+
record.decode_ok
|
|
424
|
+
&& record.variant == VARIANT_DATA
|
|
425
|
+
&& record.has_data
|
|
426
|
+
&& record.verify == VerifyStatus::Verified
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#[wasm_bindgen]
|
|
430
|
+
impl NativePeerbitBackbone {
|
|
431
|
+
/// Stashed-input twin of
|
|
432
|
+
/// `prepare_raw_receive_unverified_expected_compact_columns_batch`: the
|
|
433
|
+
/// blocks come from the wire stash (wasm memory) instead of a JS array.
|
|
434
|
+
pub fn prepare_stashed_raw_receive_expected_compact_columns_batch(
|
|
435
|
+
&mut self,
|
|
436
|
+
session: &NativeWireSyncSession,
|
|
437
|
+
id: &[u8],
|
|
438
|
+
indexes: Uint32Array,
|
|
439
|
+
hashes: Array,
|
|
440
|
+
verify_signatures: bool,
|
|
441
|
+
) -> Result<JsValue, JsValue> {
|
|
442
|
+
let indexes = indexes.to_vec();
|
|
443
|
+
let Some(blocks) = session.core.blocks(id, Some(&indexes)) else {
|
|
444
|
+
return Ok(JsValue::UNDEFINED);
|
|
445
|
+
};
|
|
446
|
+
let hashes = crate::js_interop::strings_from_array(hashes)?;
|
|
447
|
+
crate::js_interop::ensure_same_len(blocks.len(), hashes.len(), "stashed raw receive")?;
|
|
448
|
+
let prepared = self.prepare_raw_receive_entries(blocks, Some(hashes), verify_signatures)?;
|
|
449
|
+
Ok(self
|
|
450
|
+
.prepare_raw_receive_columns_from_entries(prepared, false, false)?
|
|
451
|
+
.into())
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// Stashed-input twin of
|
|
455
|
+
/// `prepare_raw_receive_unverified_expected_compact_columns_and_selection_batch`.
|
|
456
|
+
#[allow(clippy::too_many_arguments)]
|
|
457
|
+
pub fn prepare_stashed_raw_receive_expected_compact_columns_and_selection_batch(
|
|
458
|
+
&mut self,
|
|
459
|
+
session: &NativeWireSyncSession,
|
|
460
|
+
id: &[u8],
|
|
461
|
+
indexes: Uint32Array,
|
|
462
|
+
hashes: Array,
|
|
463
|
+
min_replicas: u32,
|
|
464
|
+
max_replicas: JsValue,
|
|
465
|
+
role_age_ms: f64,
|
|
466
|
+
now: String,
|
|
467
|
+
peer_filter: JsValue,
|
|
468
|
+
expand_peer_filter: bool,
|
|
469
|
+
self_hash: String,
|
|
470
|
+
include_self: bool,
|
|
471
|
+
full_replica_fallback: bool,
|
|
472
|
+
include_strict_full_replica: bool,
|
|
473
|
+
_from_hash: String,
|
|
474
|
+
) -> Result<JsValue, JsValue> {
|
|
475
|
+
let indexes = indexes.to_vec();
|
|
476
|
+
let Some(blocks) = session.core.blocks(id, Some(&indexes)) else {
|
|
477
|
+
return Ok(JsValue::UNDEFINED);
|
|
478
|
+
};
|
|
479
|
+
let hashes = crate::js_interop::strings_from_array(hashes)?;
|
|
480
|
+
crate::js_interop::ensure_same_len(blocks.len(), hashes.len(), "stashed raw receive")?;
|
|
481
|
+
let prepared = self.prepare_raw_receive_entries(blocks, Some(hashes.clone()), false)?;
|
|
482
|
+
let columns = self.prepare_raw_receive_columns_from_entries(prepared, false, false)?;
|
|
483
|
+
let selection = self.plan_prepared_raw_receive_selection_core(
|
|
484
|
+
hashes,
|
|
485
|
+
min_replicas,
|
|
486
|
+
max_replicas,
|
|
487
|
+
role_age_ms,
|
|
488
|
+
now,
|
|
489
|
+
peer_filter,
|
|
490
|
+
expand_peer_filter,
|
|
491
|
+
self_hash,
|
|
492
|
+
include_self,
|
|
493
|
+
full_replica_fallback,
|
|
494
|
+
include_strict_full_replica,
|
|
495
|
+
)?;
|
|
496
|
+
let out = Array::new();
|
|
497
|
+
out.push(&columns);
|
|
498
|
+
out.push(&selection);
|
|
499
|
+
Ok(out.into())
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// Sync fallback for lazily materialized stash-backed heads whose stash
|
|
503
|
+
/// entry was already released: serve the raw block bytes from the pending
|
|
504
|
+
/// prepared entries or the committed block store.
|
|
505
|
+
pub fn raw_receive_block_bytes(&self, hash: &str) -> JsValue {
|
|
506
|
+
if let Some(pending) = self.pending_raw_receive_entries.get(hash) {
|
|
507
|
+
return Uint8Array::from(pending.storage_bytes()).into();
|
|
508
|
+
}
|
|
509
|
+
match self.blocks.get_ref(hash) {
|
|
510
|
+
Some(bytes) => Uint8Array::from(bytes).into(),
|
|
511
|
+
None => JsValue::UNDEFINED,
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
#[cfg(test)]
|
|
517
|
+
mod tests {
|
|
518
|
+
use super::*;
|
|
519
|
+
use peerbit_wire::sync_payload::encode_raw_exchange_sync_payload;
|
|
520
|
+
use peerbit_wire::wire::{encode_frame, MessageHeader, WireMessage};
|
|
521
|
+
|
|
522
|
+
fn sync_frame(
|
|
523
|
+
id_byte: u8,
|
|
524
|
+
topic: &str,
|
|
525
|
+
mode: Option<DeliveryMode>,
|
|
526
|
+
heads: &[(String, Vec<u8>, Vec<String>)],
|
|
527
|
+
) -> (Vec<u8>, usize, usize) {
|
|
528
|
+
let payload =
|
|
529
|
+
encode_raw_exchange_sync_payload(&[topic.to_string()], true, heads, [0, 0, 0, 0]);
|
|
530
|
+
let message = WireMessage::Data {
|
|
531
|
+
header: MessageHeader {
|
|
532
|
+
id: [id_byte; ID_LENGTH],
|
|
533
|
+
timestamp: 1,
|
|
534
|
+
session: 2,
|
|
535
|
+
expires: u64::MAX,
|
|
536
|
+
priority: Some(0),
|
|
537
|
+
response_priority: None,
|
|
538
|
+
origin: None,
|
|
539
|
+
mode,
|
|
540
|
+
signatures: Some(Vec::new()),
|
|
541
|
+
},
|
|
542
|
+
data: Some(payload.clone()),
|
|
543
|
+
};
|
|
544
|
+
let frame = encode_frame(&message);
|
|
545
|
+
let data_offset = frame.len() - payload.len();
|
|
546
|
+
(frame, data_offset, payload.len())
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fn heads() -> Vec<(String, Vec<u8>, Vec<String>)> {
|
|
550
|
+
vec![
|
|
551
|
+
("h0".to_string(), vec![1, 2, 3], Vec::new()),
|
|
552
|
+
("h1".to_string(), vec![4, 5], vec!["g".to_string()]),
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
fn silent_to_self() -> Option<DeliveryMode> {
|
|
557
|
+
Some(DeliveryMode::Silent {
|
|
558
|
+
to: vec!["self-hash".to_string()],
|
|
559
|
+
redundancy: 1,
|
|
560
|
+
})
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#[test]
|
|
564
|
+
fn stashes_registered_topic_frames_addressed_to_self() {
|
|
565
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
566
|
+
core.register_topic("topic".to_string());
|
|
567
|
+
let (frame, data_offset, data_length) = sync_frame(7, "topic", silent_to_self(), &heads());
|
|
568
|
+
let mut buffer = Some(frame.clone());
|
|
569
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
570
|
+
assert!(buffer.is_none(), "stash takes frame ownership");
|
|
571
|
+
assert_eq!(core.stash_len(), 1);
|
|
572
|
+
|
|
573
|
+
let stashed = core.get(&[7u8; ID_LENGTH]).unwrap();
|
|
574
|
+
assert_eq!(stashed.head_count(), 2);
|
|
575
|
+
assert_eq!(stashed.head(0).unwrap().hash, "h0");
|
|
576
|
+
assert_eq!(stashed.reserved(), [0, 0, 0, 0]);
|
|
577
|
+
let blocks = core.blocks(&[7u8; ID_LENGTH], None).unwrap();
|
|
578
|
+
assert_eq!(blocks, vec![vec![1, 2, 3], vec![4, 5]]);
|
|
579
|
+
let selected = core.blocks(&[7u8; ID_LENGTH], Some(&[1])).unwrap();
|
|
580
|
+
assert_eq!(selected, vec![vec![4, 5]]);
|
|
581
|
+
|
|
582
|
+
assert!(core.release(&[7u8; ID_LENGTH]));
|
|
583
|
+
assert_eq!(core.stash_len(), 0);
|
|
584
|
+
assert!(!core.release(&[7u8; ID_LENGTH]));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
fn skips_unregistered_topics_and_foreign_recipients() {
|
|
589
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
590
|
+
core.register_topic("topic".to_string());
|
|
591
|
+
|
|
592
|
+
let (frame, data_offset, data_length) =
|
|
593
|
+
sync_frame(1, "other-topic", silent_to_self(), &heads());
|
|
594
|
+
let mut buffer = Some(frame);
|
|
595
|
+
assert!(!core.try_stash(&mut buffer, data_offset, data_length));
|
|
596
|
+
assert!(buffer.is_some(), "rejected frames keep their buffer");
|
|
597
|
+
|
|
598
|
+
let relay_mode = Some(DeliveryMode::Silent {
|
|
599
|
+
to: vec!["someone-else".to_string()],
|
|
600
|
+
redundancy: 1,
|
|
601
|
+
});
|
|
602
|
+
let (frame, data_offset, data_length) = sync_frame(2, "topic", relay_mode, &heads());
|
|
603
|
+
let mut buffer = Some(frame);
|
|
604
|
+
assert!(!core.try_stash(&mut buffer, data_offset, data_length));
|
|
605
|
+
|
|
606
|
+
let (frame, data_offset, data_length) =
|
|
607
|
+
sync_frame(3, "topic", Some(DeliveryMode::AnyWhere), &heads());
|
|
608
|
+
let mut buffer = Some(frame);
|
|
609
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
610
|
+
|
|
611
|
+
core.unregister_topic("topic");
|
|
612
|
+
let (frame, data_offset, data_length) = sync_frame(4, "topic", silent_to_self(), &heads());
|
|
613
|
+
let mut buffer = Some(frame);
|
|
614
|
+
assert!(!core.try_stash(&mut buffer, data_offset, data_length));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[test]
|
|
618
|
+
fn topic_registration_is_refcounted() {
|
|
619
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
620
|
+
core.register_topic("topic".to_string());
|
|
621
|
+
core.register_topic("topic".to_string());
|
|
622
|
+
assert!(core.unregister_topic("topic"));
|
|
623
|
+
assert_eq!(core.topic_count(), 1);
|
|
624
|
+
assert!(core.unregister_topic("topic"));
|
|
625
|
+
assert_eq!(core.topic_count(), 0);
|
|
626
|
+
assert!(!core.unregister_topic("topic"));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#[test]
|
|
630
|
+
fn evicts_oldest_when_over_message_cap() {
|
|
631
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
632
|
+
core.register_topic("topic".to_string());
|
|
633
|
+
for index in 0..(WIRE_SYNC_MAX_STASHED_MESSAGES + 3) {
|
|
634
|
+
let (frame, data_offset, data_length) =
|
|
635
|
+
sync_frame(index as u8, "topic", silent_to_self(), &heads());
|
|
636
|
+
// ids repeat every 256 messages; use distinct high bytes instead
|
|
637
|
+
let mut frame = frame;
|
|
638
|
+
frame[2] = (index >> 8) as u8; // second byte of the 32-byte id
|
|
639
|
+
let mut buffer = Some(frame);
|
|
640
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
641
|
+
}
|
|
642
|
+
assert_eq!(core.stash_len(), WIRE_SYNC_MAX_STASHED_MESSAGES);
|
|
643
|
+
assert_eq!(core.counters.evicted, 3);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#[test]
|
|
647
|
+
fn pinned_entries_survive_fifo_eviction_until_release() {
|
|
648
|
+
// Regression: a message that was resolved for processing
|
|
649
|
+
// (stashed_meta pins it) must keep its block bytes available across
|
|
650
|
+
// the awaits of SharedLog.onMessage even when a burst of later
|
|
651
|
+
// frames overflows the FIFO caps — there is no TS fallback for a
|
|
652
|
+
// resolved message.
|
|
653
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
654
|
+
core.register_topic("topic".to_string());
|
|
655
|
+
let (frame, data_offset, data_length) = sync_frame(0, "topic", silent_to_self(), &heads());
|
|
656
|
+
let mut buffer = Some(frame);
|
|
657
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
658
|
+
assert!(core.pin(&[0u8; ID_LENGTH]));
|
|
659
|
+
assert!(!core.pin(&[9u8; ID_LENGTH]), "missing ids cannot be pinned");
|
|
660
|
+
|
|
661
|
+
// Flood far past the message cap; the pinned entry is oldest.
|
|
662
|
+
for index in 1..(WIRE_SYNC_MAX_STASHED_MESSAGES + 8) {
|
|
663
|
+
let (frame, data_offset, data_length) =
|
|
664
|
+
sync_frame(index as u8, "topic", silent_to_self(), &heads());
|
|
665
|
+
let mut frame = frame;
|
|
666
|
+
frame[2] = (index >> 8) as u8; // second byte of the 32-byte id
|
|
667
|
+
let mut buffer = Some(frame);
|
|
668
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
669
|
+
}
|
|
670
|
+
assert!(core.counters.evicted > 0);
|
|
671
|
+
// Pinned entry survives (the cap counts it, so the stash holds the
|
|
672
|
+
// cap's worth of unpinned entries plus the pinned one).
|
|
673
|
+
let blocks = core.blocks(&[0u8; ID_LENGTH], None).unwrap();
|
|
674
|
+
assert_eq!(blocks, vec![vec![1, 2, 3], vec![4, 5]]);
|
|
675
|
+
|
|
676
|
+
assert!(core.release(&[0u8; ID_LENGTH]));
|
|
677
|
+
assert!(core.get(&[0u8; ID_LENGTH]).is_none());
|
|
678
|
+
assert!(!core.release(&[0u8; ID_LENGTH]));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#[test]
|
|
682
|
+
fn duplicate_of_pinned_entry_keeps_the_pinned_entry() {
|
|
683
|
+
// A duplicate delivery (redundancy >= 2) of a message that is
|
|
684
|
+
// mid-processing must not replace the pinned entry — replacing would
|
|
685
|
+
// reset the pin and re-expose the in-flight message to eviction.
|
|
686
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
687
|
+
core.register_topic("topic".to_string());
|
|
688
|
+
let (frame, data_offset, data_length) = sync_frame(5, "topic", silent_to_self(), &heads());
|
|
689
|
+
let mut buffer = Some(frame.clone());
|
|
690
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
691
|
+
assert!(core.pin(&[5u8; ID_LENGTH]));
|
|
692
|
+
|
|
693
|
+
let mut buffer = Some(frame);
|
|
694
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
695
|
+
assert!(
|
|
696
|
+
buffer.is_some(),
|
|
697
|
+
"duplicates of pinned entries keep their buffer"
|
|
698
|
+
);
|
|
699
|
+
assert_eq!(core.stash_len(), 1);
|
|
700
|
+
|
|
701
|
+
// Still pinned: flooding past the cap does not evict it.
|
|
702
|
+
for index in 0..WIRE_SYNC_MAX_STASHED_MESSAGES + 1 {
|
|
703
|
+
let (frame, data_offset, data_length) =
|
|
704
|
+
sync_frame(index as u8, "topic", silent_to_self(), &heads());
|
|
705
|
+
let mut frame = frame;
|
|
706
|
+
frame[2] = 0xff; // distinct id space from the pinned entry
|
|
707
|
+
frame[3] = (index >> 8) as u8;
|
|
708
|
+
let mut buffer = Some(frame);
|
|
709
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
710
|
+
}
|
|
711
|
+
assert!(core.blocks(&[5u8; ID_LENGTH], None).is_some());
|
|
712
|
+
assert!(core.release(&[5u8; ID_LENGTH]));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
#[test]
|
|
716
|
+
fn restashing_same_id_replaces_entry() {
|
|
717
|
+
let mut core = WireSyncCore::new("self-hash".to_string());
|
|
718
|
+
core.register_topic("topic".to_string());
|
|
719
|
+
let (frame, data_offset, data_length) = sync_frame(9, "topic", silent_to_self(), &heads());
|
|
720
|
+
let mut buffer = Some(frame.clone());
|
|
721
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
722
|
+
let mut buffer = Some(frame);
|
|
723
|
+
assert!(core.try_stash(&mut buffer, data_offset, data_length));
|
|
724
|
+
assert_eq!(core.stash_len(), 1);
|
|
725
|
+
assert_eq!(core.counters.stashed, 2);
|
|
726
|
+
}
|
|
727
|
+
}
|