Skip to content

Commit a37485d

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

11 files changed

Lines changed: 3860 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
@@ -11,6 +11,7 @@ members = [
1111
"document/format",
1212
"document/graph-storage",
1313
"document/container",
14+
"document/peer-transport",
1415
"editor",
1516
"frontend/wrapper",
1617
"libraries/dyn-any",
@@ -103,6 +104,7 @@ graph-craft = { path = "node-graph/graph-craft" }
103104
document-format = { path = "document/format" }
104105
document-graph-storage = { path = "document/graph-storage", default-features = false }
105106
document-container = { path = "document/container" }
107+
peer-transport = { path = "document/peer-transport" }
106108
raster-nodes = { path = "node-graph/nodes/raster" }
107109
graphene-std = { path = "node-graph/nodes/gstd" }
108110
interpreted-executor = { path = "node-graph/interpreted-executor" }
@@ -122,6 +124,7 @@ bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] }
122124
serde = { version = "1.0", features = ["derive", "rc"] }
123125
serde_json = "1.0"
124126
postcard = { version = "1.1", default-features = false, features = ["use-std"] }
127+
rmp-serde = "1.3"
125128
serde_bytes = "0.11"
126129
serde-wasm-bindgen = "0.6"
127130
reqwest = { version = "0.13", features = ["blocking", "json"] }

‎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: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
/// Deliver everything in flight to one peer only.
96+
pub fn deliver_to(&mut self, to: TransportPeerId) {
97+
let mut shared = self.shared.lock().unwrap();
98+
let queues: Vec<_> = shared.in_flight.keys().copied().filter(|&(_, receiver)| receiver == to).collect();
99+
100+
for key in queues {
101+
let packets = shared.in_flight.get_mut(&key).map(std::mem::take).unwrap_or_default();
102+
for packet in packets {
103+
shared.inboxes.entry(to).or_default().push_back(TransportEvent::Packet(key.0, packet));
104+
}
105+
}
106+
}
107+
108+
pub fn random_below(&mut self, bound: usize) -> usize {
109+
self.rng.below(bound)
110+
}
111+
}
112+
113+
/// Like a real socket, an endpoint sends only to the peers whose connect events it has polled.
114+
pub struct MockEndpoint {
115+
id: TransportPeerId,
116+
shared: Arc<Mutex<Shared>>,
117+
known_peers: Vec<TransportPeerId>,
118+
}
119+
120+
impl MockEndpoint {
121+
pub fn id(&self) -> TransportPeerId {
122+
self.id
123+
}
124+
}
125+
126+
impl Transport for MockEndpoint {
127+
fn send(&mut self, to: TransportPeerId, packet: &SyncPacket) -> Result<(), PacketError> {
128+
packet.encode()?;
129+
let mut shared = self.shared.lock().unwrap();
130+
// A link that dropped since the last poll loses the packet, as a real socket does.
131+
if self.known_peers.contains(&to) && shared.connected.contains(&self.id) && shared.connected.contains(&to) {
132+
shared.in_flight.entry((self.id, to)).or_default().push_back(packet.clone());
133+
}
134+
Ok(())
135+
}
136+
137+
fn broadcast_except(&mut self, excluded: Option<TransportPeerId>, packet: &SyncPacket) -> Result<(), PacketError> {
138+
for to in self.known_peers.clone() {
139+
if Some(to) != excluded {
140+
self.send(to, packet)?;
141+
}
142+
}
143+
Ok(())
144+
}
145+
146+
fn poll(&mut self) -> Vec<TransportEvent> {
147+
let events: Vec<_> = self.shared.lock().unwrap().inboxes.get_mut(&self.id).map(|inbox| inbox.drain(..).collect()).unwrap_or_default();
148+
for event in &events {
149+
match event {
150+
TransportEvent::PeerConnected(peer) => self.known_peers.push(*peer),
151+
TransportEvent::PeerDisconnected(peer) => self.known_peers.retain(|known| known != peer),
152+
_ => {}
153+
}
154+
}
155+
events
156+
}
157+
158+
fn close(&mut self) {
159+
self.shared.lock().unwrap().disconnect(self.id);
160+
}
161+
}
162+
163+
struct SplitMix64(u64);
164+
165+
impl SplitMix64 {
166+
fn next(&mut self) -> u64 {
167+
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
168+
let mut z = self.0;
169+
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
170+
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
171+
z ^ (z >> 31)
172+
}
173+
174+
fn below(&mut self, bound: usize) -> usize {
175+
(self.next() % bound as u64) as usize
176+
}
177+
}

0 commit comments

Comments
 (0)