Skip to content

Commit e103486

Browse files
committed
Add the peer-transport crate syncing document sessions between peers
1 parent dc8cf0d commit e103486

13 files changed

Lines changed: 3851 additions & 48 deletions

File tree

‎Cargo.lock‎

Lines changed: 1691 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ members = [
1717
"document/migrations/formats",
1818
"document/migrations/snapshot",
1919
"document/migrations/nodes",
20+
"document/peer-transport",
2021
"editor",
2122
"frontend/wrapper",
2223
"libraries/dyn-any",
@@ -114,6 +115,7 @@ migration-macros = { path = "document/migrations/macros" }
114115
migration-runner = { path = "document/migrations/runner" }
115116
migration-formats = { path = "document/migrations/formats" }
116117
migration-nodes = { path = "document/migrations/nodes" }
118+
peer-transport = { path = "document/peer-transport" }
117119
raster-nodes = { path = "node-graph/nodes/raster" }
118120
graphene-std = { path = "node-graph/nodes/gstd" }
119121
interpreted-executor = { path = "node-graph/interpreted-executor" }
@@ -133,6 +135,7 @@ bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] }
133135
serde = { version = "1.0", features = ["derive", "rc"] }
134136
serde_json = "1.0"
135137
postcard = { version = "1.1", default-features = false, features = ["use-std"] }
138+
rmp-serde = "1.3"
136139
serde_bytes = "0.11"
137140
serde-wasm-bindgen = "0.6"
138141
reqwest = { version = "0.13", features = ["blocking", "json"] }

‎about.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ accepted = [
1414
"OpenSSL", # Keep this list in sync with those in `/deny.toml`
1515
"Unicode-3.0", # Keep this list in sync with those in `/deny.toml`
1616
"Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml`
17+
"Unlicense", # Keep this list in sync with those in `/deny.toml`
1718
"Zlib", # Keep this list in sync with those in `/deny.toml`
1819
"NCSA", # Keep this list in sync with those in `/deny.toml`
1920
"bzip2-1.0.6", # Keep this list in sync with those in `/deny.toml`

‎deny.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ allow = [
7878
"OpenSSL", # Keep this list in sync with those in `/about.toml`
7979
"Unicode-3.0", # Keep this list in sync with those in `/about.toml`
8080
"Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml`
81+
"Unlicense", # Keep this list in sync with those in `/about.toml`
8182
"Zlib", # Keep this list in sync with those in `/about.toml`
8283
"NCSA", # Keep this list in sync with those in `/about.toml`
8384
"bzip2-1.0.6", # Keep this list in sync with those in `/about.toml`

‎document/peer-transport/Cargo.toml‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "peer-transport"
3+
description = "Peer-to-peer sync of hot ops and retired history between document-graph-storage sessions over WebRTC"
4+
rust-version.workspace = true
5+
edition.workspace = true
6+
authors.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
license.workspace = true
10+
version.workspace = true
11+
publish.workspace = true
12+
13+
[dependencies]
14+
core-types = { workspace = true }
15+
document-graph-storage = { workspace = true }
16+
matchbox_socket = "0.14.0"
17+
futures = { workspace = true }
18+
log = { workspace = true }
19+
serde = { workspace = true }
20+
serde_bytes = { workspace = true }
21+
rmp-serde = { workspace = true }
22+
base64 = { workspace = true }
23+
thiserror = { workspace = true }
24+
uuid = "1"
25+
26+
[target.'cfg(not(target_family = "wasm"))'.dependencies]
27+
rustls = { version = "0.23", default-features = false, features = ["ring"] }
28+
29+
[lints]
30+
workspace = true

‎document/peer-transport/src/lib.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pub mod mock;
2+
mod packet;
3+
mod replica;
4+
mod room;
5+
mod target;
6+
mod token;
7+
mod transport;
8+
9+
pub use packet::{CursorPosition, CursorSpace, MAX_PACKET_BYTES, PacketError, Role, SyncPacket, SyncPayload};
10+
pub use replica::{Event, RemotePeer, Replica, ReplicaError};
11+
pub use room::{Incoming, Room};
12+
pub use target::{SyncTarget, TargetError};
13+
pub use token::{InvalidSessionToken, SessionToken};
14+
pub use transport::{Transport, TransportEvent, TransportPeerId};
15+
16+
pub use matchbox_socket::MessageLoopFuture;
17+
18+
/// Matchbox signaling server for sessions, behind TLS so browsers on HTTPS pages can reach it.
19+
pub const DEFAULT_SIGNALING_SERVER: &str = "wss://graphite.kobert.dev";
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//! In-process transport for deterministic simulation. Packets wait in per-pair queues until `step` delivers
2+
//! one, so the seed drives interleavings while each pair stays ordered like a reliable channel.
3+
4+
use crate::packet::{PacketError, SyncPacket};
5+
use crate::transport::{Transport, TransportEvent, TransportPeerId};
6+
use std::collections::{BTreeMap, HashMap, VecDeque};
7+
use std::sync::{Arc, Mutex};
8+
9+
#[derive(Default)]
10+
struct Shared {
11+
connected: Vec<TransportPeerId>,
12+
inboxes: HashMap<TransportPeerId, VecDeque<TransportEvent>>,
13+
in_flight: BTreeMap<(TransportPeerId, TransportPeerId), VecDeque<SyncPacket>>,
14+
next_peer: u128,
15+
}
16+
17+
impl Shared {
18+
fn disconnect(&mut self, id: TransportPeerId) {
19+
if !self.connected.contains(&id) {
20+
return;
21+
}
22+
self.connected.retain(|&other| other != id);
23+
self.in_flight.retain(|&(from, to), _| from != id && to != id);
24+
for &other in &self.connected.clone() {
25+
self.inboxes.entry(other).or_default().push_back(TransportEvent::PeerDisconnected(id));
26+
self.inboxes.entry(id).or_default().push_back(TransportEvent::PeerDisconnected(other));
27+
}
28+
}
29+
}
30+
31+
pub struct MockNetwork {
32+
shared: Arc<Mutex<Shared>>,
33+
rng: SplitMix64,
34+
}
35+
36+
impl MockNetwork {
37+
pub fn new(seed: u64) -> Self {
38+
Self {
39+
shared: Arc::new(Mutex::new(Shared::default())),
40+
rng: SplitMix64(seed),
41+
}
42+
}
43+
44+
/// A new, not yet connected peer.
45+
pub fn endpoint(&mut self) -> MockEndpoint {
46+
let mut shared = self.shared.lock().unwrap();
47+
shared.next_peer += 1;
48+
let id = TransportPeerId(uuid::Uuid::from_u128(shared.next_peer));
49+
shared.inboxes.insert(id, VecDeque::new());
50+
MockEndpoint {
51+
id,
52+
shared: self.shared.clone(),
53+
known_peers: Vec::new(),
54+
}
55+
}
56+
57+
pub fn connect(&mut self, id: TransportPeerId) {
58+
let mut shared = self.shared.lock().unwrap();
59+
if shared.connected.contains(&id) {
60+
return;
61+
}
62+
for &other in &shared.connected.clone() {
63+
shared.inboxes.entry(other).or_default().push_back(TransportEvent::PeerConnected(id));
64+
shared.inboxes.entry(id).or_default().push_back(TransportEvent::PeerConnected(other));
65+
}
66+
shared.connected.push(id);
67+
}
68+
69+
pub fn disconnect(&mut self, id: TransportPeerId) {
70+
self.shared.lock().unwrap().disconnect(id);
71+
}
72+
73+
pub fn pending(&self) -> usize {
74+
self.shared.lock().unwrap().in_flight.values().map(VecDeque::len).sum()
75+
}
76+
77+
/// Deliver one in-flight packet from a randomly chosen sender/receiver pair.
78+
pub fn step(&mut self) -> bool {
79+
let mut shared = self.shared.lock().unwrap();
80+
let queues: Vec<_> = shared.in_flight.iter().filter(|(_, queue)| !queue.is_empty()).map(|(&key, _)| key).collect();
81+
if queues.is_empty() {
82+
return false;
83+
}
84+
85+
let (from, to) = queues[self.rng.below(queues.len())];
86+
let packet = shared.in_flight.get_mut(&(from, to)).and_then(VecDeque::pop_front).expect("queue is non-empty");
87+
shared.inboxes.entry(to).or_default().push_back(TransportEvent::Packet(from, packet));
88+
true
89+
}
90+
91+
pub fn deliver_all(&mut self) {
92+
while self.step() {}
93+
}
94+
95+
pub fn deliver_to(&mut self, to: TransportPeerId) {
96+
let mut shared = self.shared.lock().unwrap();
97+
let queues: Vec<_> = shared.in_flight.keys().copied().filter(|&(_, receiver)| receiver == to).collect();
98+
99+
for key in queues {
100+
let packets = shared.in_flight.get_mut(&key).map(std::mem::take).unwrap_or_default();
101+
for packet in packets {
102+
shared.inboxes.entry(to).or_default().push_back(TransportEvent::Packet(key.0, packet));
103+
}
104+
}
105+
}
106+
107+
pub fn random_below(&mut self, bound: usize) -> usize {
108+
self.rng.below(bound)
109+
}
110+
}
111+
112+
/// Like a real socket, an endpoint sends only to the peers whose connect events it has polled.
113+
pub struct MockEndpoint {
114+
id: TransportPeerId,
115+
shared: Arc<Mutex<Shared>>,
116+
known_peers: Vec<TransportPeerId>,
117+
}
118+
119+
impl MockEndpoint {
120+
pub fn id(&self) -> TransportPeerId {
121+
self.id
122+
}
123+
}
124+
125+
impl Transport for MockEndpoint {
126+
fn send(&mut self, to: TransportPeerId, packet: &SyncPacket) -> Result<(), PacketError> {
127+
packet.encode()?;
128+
let mut shared = self.shared.lock().unwrap();
129+
// A link that dropped since the last poll loses the packet, as a real socket does.
130+
if self.known_peers.contains(&to) && shared.connected.contains(&self.id) && shared.connected.contains(&to) {
131+
shared.in_flight.entry((self.id, to)).or_default().push_back(packet.clone());
132+
}
133+
Ok(())
134+
}
135+
136+
fn broadcast_except(&mut self, excluded: Option<TransportPeerId>, packet: &SyncPacket) -> Result<(), PacketError> {
137+
for to in self.known_peers.clone() {
138+
if Some(to) != excluded {
139+
self.send(to, packet)?;
140+
}
141+
}
142+
Ok(())
143+
}
144+
145+
fn poll(&mut self) -> Vec<TransportEvent> {
146+
let events: Vec<_> = self.shared.lock().unwrap().inboxes.get_mut(&self.id).map(|inbox| inbox.drain(..).collect()).unwrap_or_default();
147+
for event in &events {
148+
match event {
149+
TransportEvent::PeerConnected(peer) => self.known_peers.push(*peer),
150+
TransportEvent::PeerDisconnected(peer) => self.known_peers.retain(|known| known != peer),
151+
_ => {}
152+
}
153+
}
154+
events
155+
}
156+
157+
fn close(&mut self) {
158+
self.shared.lock().unwrap().disconnect(self.id);
159+
}
160+
}
161+
162+
struct SplitMix64(u64);
163+
164+
impl SplitMix64 {
165+
fn next(&mut self) -> u64 {
166+
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
167+
let mut z = self.0;
168+
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
169+
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
170+
z ^ (z >> 31)
171+
}
172+
173+
fn below(&mut self, bound: usize) -> usize {
174+
(self.next() % bound as u64) as usize
175+
}
176+
}

0 commit comments

Comments
 (0)