yrb-lite 0.1.0.beta1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.toml +3 -0
- data/LICENSE +21 -0
- data/README.md +427 -0
- data/ext/yrb_lite/Cargo.toml +19 -0
- data/ext/yrb_lite/extconf.rb +6 -0
- data/ext/yrb_lite/src/lib.rs +752 -0
- data/lib/yrb-lite.rb +4 -0
- data/lib/yrb_lite/sync.rb +456 -0
- data/lib/yrb_lite/version.rb +5 -0
- data/lib/yrb_lite.rb +20 -0
- metadata +130 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
use magnus::{function, method, prelude::*, Error, RString, Ruby, TryConvert, Value};
|
|
2
|
+
use yrs::encoding::read::{Cursor, Read};
|
|
3
|
+
use yrs::sync::protocol::MessageReader;
|
|
4
|
+
use yrs::sync::{Awareness, DefaultProtocol, Message, Protocol, SyncMessage};
|
|
5
|
+
use yrs::updates::decoder::{Decode, DecoderV1};
|
|
6
|
+
use yrs::updates::encoder::{Encode, Encoder, EncoderV1};
|
|
7
|
+
use yrs::{Doc, ReadTxn, Transact};
|
|
8
|
+
|
|
9
|
+
/// Wrapper around yrs Doc.
|
|
10
|
+
///
|
|
11
|
+
/// Thread safety: `yrs::Doc` is `Send + Sync`. Its `transact()`/`transact_mut()`
|
|
12
|
+
/// acquire an internal RwLock with blocking semantics, so concurrent access from
|
|
13
|
+
/// multiple Ruby threads serializes safely instead of panicking. There's no
|
|
14
|
+
/// interior-mutability wrapper (RefCell and friends): every method opens and
|
|
15
|
+
/// closes its transaction within a single call.
|
|
16
|
+
#[magnus::wrap(class = "YrbLite::Doc", free_immediately, size)]
|
|
17
|
+
struct RbDoc(Doc);
|
|
18
|
+
|
|
19
|
+
/// Wrapper around yrs Awareness (which contains a Doc).
|
|
20
|
+
///
|
|
21
|
+
/// Thread safety: `yrs::sync::Awareness` keeps client states in a `DashMap`
|
|
22
|
+
/// and exposes everything through `&self`, so it's built for multi-threaded
|
|
23
|
+
/// server use.
|
|
24
|
+
#[magnus::wrap(class = "YrbLite::Awareness", free_immediately, size)]
|
|
25
|
+
struct RbAwareness(Awareness);
|
|
26
|
+
|
|
27
|
+
/// Compile-time proof that the wrapped types are thread-safe. If a future
|
|
28
|
+
/// yrs upgrade makes Doc or Awareness lose Send/Sync, this fails the build
|
|
29
|
+
/// instead of silently shipping a thread-unsafe gem.
|
|
30
|
+
#[allow(dead_code)]
|
|
31
|
+
fn assert_thread_safe() {
|
|
32
|
+
fn is_send_sync<T: Send + Sync>() {}
|
|
33
|
+
is_send_sync::<Doc>();
|
|
34
|
+
is_send_sync::<Awareness>();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Run `f` with the GVL (Global VM Lock) released, so other Ruby threads,
|
|
38
|
+
/// including ones calling into this extension, can run in parallel.
|
|
39
|
+
///
|
|
40
|
+
/// Safety rules for the closure:
|
|
41
|
+
/// - It must not touch any Ruby object or call any Ruby API. Inputs are copied
|
|
42
|
+
/// out of Ruby strings before entering, and results are converted to Ruby
|
|
43
|
+
/// objects after returning.
|
|
44
|
+
/// - It must be `Send` (it runs while other threads own the GVL). `&Doc` and
|
|
45
|
+
/// `&Awareness` are fine: both types are `Sync` (asserted above).
|
|
46
|
+
/// - Any doc lock it takes must be acquired and released inside the closure, so
|
|
47
|
+
/// we never reacquire the GVL while holding a yrs lock and can't deadlock on
|
|
48
|
+
/// lock order.
|
|
49
|
+
///
|
|
50
|
+
/// Panics inside the closure are caught and re-raised (resumed) after the GVL
|
|
51
|
+
/// is reacquired, where magnus converts them to Ruby exceptions.
|
|
52
|
+
fn nogvl<F, R>(f: F) -> R
|
|
53
|
+
where
|
|
54
|
+
F: FnOnce() -> R + Send,
|
|
55
|
+
R: Send,
|
|
56
|
+
{
|
|
57
|
+
use std::ffi::c_void;
|
|
58
|
+
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
|
|
59
|
+
|
|
60
|
+
struct Ctx<F, R> {
|
|
61
|
+
func: Option<F>,
|
|
62
|
+
result: Option<std::thread::Result<R>>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
unsafe extern "C" fn callback<F, R>(arg: *mut c_void) -> *mut c_void
|
|
66
|
+
where
|
|
67
|
+
F: FnOnce() -> R,
|
|
68
|
+
{
|
|
69
|
+
let ctx = &mut *(arg as *mut Ctx<F, R>);
|
|
70
|
+
let func = ctx.func.take().expect("nogvl callback invoked twice");
|
|
71
|
+
ctx.result = Some(catch_unwind(AssertUnwindSafe(func)));
|
|
72
|
+
std::ptr::null_mut()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let mut ctx: Ctx<F, R> = Ctx {
|
|
76
|
+
func: Some(f),
|
|
77
|
+
result: None,
|
|
78
|
+
};
|
|
79
|
+
unsafe {
|
|
80
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
81
|
+
Some(callback::<F, R>),
|
|
82
|
+
&mut ctx as *mut Ctx<F, R> as *mut c_void,
|
|
83
|
+
None,
|
|
84
|
+
std::ptr::null_mut(),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
match ctx.result.expect("nogvl callback did not run") {
|
|
88
|
+
Ok(result) => result,
|
|
89
|
+
Err(panic) => resume_unwind(panic),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Helper to create a binary Ruby string from bytes. Called only with the GVL
|
|
94
|
+
/// held (after the native work finishes), so `Ruby::get` always succeeds.
|
|
95
|
+
fn binary_string(bytes: &[u8]) -> RString {
|
|
96
|
+
let ruby = Ruby::get().unwrap();
|
|
97
|
+
let s = ruby.str_from_slice(bytes);
|
|
98
|
+
let _ = s.enc_associate(ruby.ascii8bit_encindex());
|
|
99
|
+
s
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Copy a Ruby string's bytes so they can be used without the GVL.
|
|
103
|
+
fn copy_bytes(s: RString) -> Vec<u8> {
|
|
104
|
+
unsafe { s.as_slice() }.to_vec()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn runtime_error(msg: String) -> Error {
|
|
108
|
+
Error::new(Ruby::get().unwrap().exception_runtime_error(), msg)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Pure protocol helpers (no Ruby, no GVL); unit-tested in the `tests` module.
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
/// Classify a frame: a non-zero code only for exactly one well-formed message
|
|
116
|
+
/// that consumes the whole buffer (see `RbAwareness::message_kind` for codes).
|
|
117
|
+
fn classify_message(bytes: &[u8]) -> u8 {
|
|
118
|
+
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
119
|
+
let msg = match Message::decode(&mut decoder) {
|
|
120
|
+
Ok(msg) => msg,
|
|
121
|
+
Err(_) => return 0, // empty or malformed
|
|
122
|
+
};
|
|
123
|
+
// Any remaining byte means a second message or trailing garbage.
|
|
124
|
+
if decoder.read_u8().is_ok() {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
match msg {
|
|
128
|
+
Message::Sync(SyncMessage::SyncStep1(_)) => 1,
|
|
129
|
+
Message::Sync(SyncMessage::SyncStep2(_)) | Message::Sync(SyncMessage::Update(_)) => 2,
|
|
130
|
+
Message::Awareness(_) => 3,
|
|
131
|
+
Message::AwarenessQuery => 4,
|
|
132
|
+
_ => 0, // Auth / Custom: not part of our model
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Merge the document-update deltas (Update / SyncStep2 payloads) carried by a
|
|
137
|
+
/// frame into one update, or `None` if the frame carries no document change
|
|
138
|
+
/// (a request, an awareness update, or a no-op handshake SyncStep2).
|
|
139
|
+
fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
|
|
140
|
+
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
141
|
+
let mut updates: Vec<Vec<u8>> = Vec::new();
|
|
142
|
+
for msg in MessageReader::new(&mut decoder) {
|
|
143
|
+
match msg.map_err(|e| e.to_string())? {
|
|
144
|
+
Message::Sync(SyncMessage::Update(u)) | Message::Sync(SyncMessage::SyncStep2(u)) => {
|
|
145
|
+
updates.push(u)
|
|
146
|
+
}
|
|
147
|
+
_ => {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let merged = match updates.len() {
|
|
151
|
+
0 => return Ok(None),
|
|
152
|
+
1 => updates.pop().unwrap(),
|
|
153
|
+
_ => yrs::merge_updates_v1(&updates).map_err(|e| e.to_string())?,
|
|
154
|
+
};
|
|
155
|
+
let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
|
|
156
|
+
if update.state_vector().is_empty() && update.delete_set().is_empty() {
|
|
157
|
+
return Ok(None); // no-op (e.g. the empty SyncStep2 in an opening handshake)
|
|
158
|
+
}
|
|
159
|
+
Ok(Some(merged))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Collect the awareness client IDs referenced by a frame's awareness messages.
|
|
163
|
+
fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
|
|
164
|
+
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
165
|
+
let mut ids = Vec::new();
|
|
166
|
+
for msg in MessageReader::new(&mut decoder) {
|
|
167
|
+
if let Message::Awareness(update) = msg.map_err(|e| e.to_string())? {
|
|
168
|
+
ids.extend(update.clients.keys().copied());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
Ok(ids)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Doc Implementation
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
impl RbDoc {
|
|
179
|
+
/// Create a new Doc with an optional client_id
|
|
180
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
181
|
+
let doc = if args.is_empty() {
|
|
182
|
+
Doc::new()
|
|
183
|
+
} else {
|
|
184
|
+
let client_id: u64 = TryConvert::try_convert(args[0])?;
|
|
185
|
+
Doc::with_client_id(client_id)
|
|
186
|
+
};
|
|
187
|
+
Ok(RbDoc(doc))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Get the client ID
|
|
191
|
+
fn client_id(&self) -> u64 {
|
|
192
|
+
self.0.client_id()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Get the document GUID
|
|
196
|
+
fn guid(&self) -> String {
|
|
197
|
+
self.0.guid().to_string()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Get the current state vector encoded as bytes
|
|
201
|
+
fn encode_state_vector(&self) -> RString {
|
|
202
|
+
let doc = &self.0;
|
|
203
|
+
let sv = nogvl(move || {
|
|
204
|
+
let txn = doc.transact();
|
|
205
|
+
txn.state_vector().encode_v1()
|
|
206
|
+
});
|
|
207
|
+
binary_string(&sv)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Encode state as update (optionally diffed against a state vector)
|
|
211
|
+
fn encode_state_as_update(&self, args: &[Value]) -> Result<RString, Error> {
|
|
212
|
+
let sv_bytes: Option<Vec<u8>> = if args.is_empty() {
|
|
213
|
+
None
|
|
214
|
+
} else {
|
|
215
|
+
let sv_string: RString = TryConvert::try_convert(args[0])?;
|
|
216
|
+
Some(copy_bytes(sv_string))
|
|
217
|
+
};
|
|
218
|
+
let doc = &self.0;
|
|
219
|
+
let update = nogvl(move || -> Result<Vec<u8>, String> {
|
|
220
|
+
let sv = match &sv_bytes {
|
|
221
|
+
None => yrs::StateVector::default(),
|
|
222
|
+
Some(bytes) => yrs::StateVector::decode_v1(bytes).map_err(|e| e.to_string())?,
|
|
223
|
+
};
|
|
224
|
+
let txn = doc.transact();
|
|
225
|
+
Ok(txn.encode_state_as_update_v1(&sv))
|
|
226
|
+
})
|
|
227
|
+
.map_err(runtime_error)?;
|
|
228
|
+
Ok(binary_string(&update))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Apply a V1 update to the document
|
|
232
|
+
fn apply_update(&self, update: RString) -> Result<(), Error> {
|
|
233
|
+
let update_bytes = copy_bytes(update);
|
|
234
|
+
let doc = &self.0;
|
|
235
|
+
nogvl(move || -> Result<(), String> {
|
|
236
|
+
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
237
|
+
let mut txn = doc.transact_mut();
|
|
238
|
+
txn.apply_update(update).map_err(|e| e.to_string())
|
|
239
|
+
})
|
|
240
|
+
.map_err(runtime_error)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Sync step 1: Create a sync message with our state vector
|
|
244
|
+
fn sync_step1(&self) -> RString {
|
|
245
|
+
let doc = &self.0;
|
|
246
|
+
let encoded = nogvl(move || {
|
|
247
|
+
let txn = doc.transact();
|
|
248
|
+
let sv = txn.state_vector();
|
|
249
|
+
Message::Sync(SyncMessage::SyncStep1(sv)).encode_v1()
|
|
250
|
+
});
|
|
251
|
+
binary_string(&encoded)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Sync step 2: Create a sync message with updates for the given state vector
|
|
255
|
+
fn sync_step2(&self, sv_bytes: RString) -> Result<RString, Error> {
|
|
256
|
+
let sv_data = copy_bytes(sv_bytes);
|
|
257
|
+
let doc = &self.0;
|
|
258
|
+
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
259
|
+
let sv = yrs::StateVector::decode_v1(&sv_data).map_err(|e| e.to_string())?;
|
|
260
|
+
let txn = doc.transact();
|
|
261
|
+
let update = txn.encode_state_as_update_v1(&sv);
|
|
262
|
+
Ok(Message::Sync(SyncMessage::SyncStep2(update)).encode_v1())
|
|
263
|
+
})
|
|
264
|
+
.map_err(runtime_error)?;
|
|
265
|
+
Ok(binary_string(&encoded))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Handle a sync message and return response (if any)
|
|
269
|
+
/// Returns [message_type, sync_type, response_bytes] or nil
|
|
270
|
+
fn handle_sync_message(&self, data: RString) -> Result<Option<(u8, u8, RString)>, Error> {
|
|
271
|
+
let data_bytes = copy_bytes(data);
|
|
272
|
+
let doc = &self.0;
|
|
273
|
+
|
|
274
|
+
let (msg_type, sync_type, response) =
|
|
275
|
+
nogvl(move || -> Result<(u8, u8, Vec<u8>), String> {
|
|
276
|
+
let msg = Message::decode_v1(&data_bytes).map_err(|e| e.to_string())?;
|
|
277
|
+
|
|
278
|
+
match msg {
|
|
279
|
+
Message::Sync(sync_msg) => match sync_msg {
|
|
280
|
+
SyncMessage::SyncStep1(sv) => {
|
|
281
|
+
// Respond with SyncStep2
|
|
282
|
+
let txn = doc.transact();
|
|
283
|
+
let update = txn.encode_state_as_update_v1(&sv);
|
|
284
|
+
let response = Message::Sync(SyncMessage::SyncStep2(update));
|
|
285
|
+
Ok((0, 0, response.encode_v1()))
|
|
286
|
+
}
|
|
287
|
+
SyncMessage::SyncStep2(update_bytes) => {
|
|
288
|
+
// Apply the update
|
|
289
|
+
let update =
|
|
290
|
+
yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
291
|
+
let mut txn = doc.transact_mut();
|
|
292
|
+
txn.apply_update(update).map_err(|e| e.to_string())?;
|
|
293
|
+
Ok((0, 1, Vec::new()))
|
|
294
|
+
}
|
|
295
|
+
SyncMessage::Update(update_bytes) => {
|
|
296
|
+
// Apply the update
|
|
297
|
+
let update =
|
|
298
|
+
yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
299
|
+
let mut txn = doc.transact_mut();
|
|
300
|
+
txn.apply_update(update).map_err(|e| e.to_string())?;
|
|
301
|
+
Ok((0, 2, Vec::new()))
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
Message::Awareness(_) => Ok((1, 0, Vec::new())),
|
|
305
|
+
Message::AwarenessQuery => Ok((3, 0, Vec::new())),
|
|
306
|
+
Message::Auth(_) => Ok((2, 0, Vec::new())),
|
|
307
|
+
Message::Custom(tag, _) => Ok((tag, 0, Vec::new())),
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
.map_err(runtime_error)?;
|
|
311
|
+
|
|
312
|
+
Ok(Some((msg_type, sync_type, binary_string(&response))))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Encode raw update bytes as a sync Update message
|
|
316
|
+
fn encode_update_message(&self, update: RString) -> RString {
|
|
317
|
+
let update_bytes = unsafe { update.as_slice() };
|
|
318
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes.to_vec()));
|
|
319
|
+
binary_string(&msg.encode_v1())
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// Awareness Implementation (includes Doc + presence)
|
|
326
|
+
// ============================================================================
|
|
327
|
+
|
|
328
|
+
impl RbAwareness {
|
|
329
|
+
/// Create a new Awareness with an optional client_id
|
|
330
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
331
|
+
let awareness = if args.is_empty() {
|
|
332
|
+
Awareness::new(Doc::new())
|
|
333
|
+
} else {
|
|
334
|
+
let client_id: u64 = TryConvert::try_convert(args[0])?;
|
|
335
|
+
Awareness::new(Doc::with_client_id(client_id))
|
|
336
|
+
};
|
|
337
|
+
Ok(RbAwareness(awareness))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Get the client ID of the underlying document
|
|
341
|
+
fn client_id(&self) -> u64 {
|
|
342
|
+
self.0.doc().client_id()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// Get the document GUID
|
|
346
|
+
fn guid(&self) -> String {
|
|
347
|
+
self.0.doc().guid().to_string()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// A standalone SyncStep1 message (the server's state vector). Sent as its
|
|
351
|
+
/// own frame in the opening handshake so providers that parse one message
|
|
352
|
+
/// per frame (e.g. @y-rb/actioncable) handle it correctly.
|
|
353
|
+
fn sync_step1(&self) -> RString {
|
|
354
|
+
let awareness = &self.0;
|
|
355
|
+
let encoded = nogvl(move || {
|
|
356
|
+
let txn = awareness.doc().transact();
|
|
357
|
+
let sv = txn.state_vector();
|
|
358
|
+
Message::Sync(SyncMessage::SyncStep1(sv)).encode_v1()
|
|
359
|
+
});
|
|
360
|
+
binary_string(&encoded)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Create initial sync messages to send when connection opens.
|
|
364
|
+
/// Returns binary data containing SyncStep1 + Awareness update.
|
|
365
|
+
fn start(&self) -> Result<RString, Error> {
|
|
366
|
+
let awareness = &self.0;
|
|
367
|
+
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
368
|
+
let protocol = DefaultProtocol;
|
|
369
|
+
let mut encoder = EncoderV1::new();
|
|
370
|
+
protocol
|
|
371
|
+
.start(awareness, &mut encoder)
|
|
372
|
+
.map_err(|e| e.to_string())?;
|
|
373
|
+
Ok(encoder.to_vec())
|
|
374
|
+
})
|
|
375
|
+
.map_err(runtime_error)?;
|
|
376
|
+
Ok(binary_string(&encoded))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/// Handle incoming message and return response messages (if any).
|
|
380
|
+
/// Returns binary data containing response messages, or empty if no response needed.
|
|
381
|
+
fn handle(&self, data: RString) -> Result<RString, Error> {
|
|
382
|
+
let data_bytes = copy_bytes(data);
|
|
383
|
+
let awareness = &self.0;
|
|
384
|
+
|
|
385
|
+
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
386
|
+
let protocol = DefaultProtocol;
|
|
387
|
+
let responses = protocol
|
|
388
|
+
.handle(awareness, &data_bytes)
|
|
389
|
+
.map_err(|e| e.to_string())?;
|
|
390
|
+
|
|
391
|
+
if responses.is_empty() {
|
|
392
|
+
return Ok(Vec::new());
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let mut encoder = EncoderV1::new();
|
|
396
|
+
for msg in responses {
|
|
397
|
+
msg.encode(&mut encoder);
|
|
398
|
+
}
|
|
399
|
+
Ok(encoder.to_vec())
|
|
400
|
+
})
|
|
401
|
+
.map_err(runtime_error)?;
|
|
402
|
+
Ok(binary_string(&encoded))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Encode an update message for broadcasting changes to peers.
|
|
406
|
+
fn encode_update(&self, update: RString) -> RString {
|
|
407
|
+
let update_bytes = unsafe { update.as_slice() };
|
|
408
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes.to_vec()));
|
|
409
|
+
binary_string(&msg.encode_v1())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Get the current state vector encoded as bytes
|
|
413
|
+
fn encode_state_vector(&self) -> RString {
|
|
414
|
+
let awareness = &self.0;
|
|
415
|
+
let sv = nogvl(move || {
|
|
416
|
+
let txn = awareness.doc().transact();
|
|
417
|
+
txn.state_vector().encode_v1()
|
|
418
|
+
});
|
|
419
|
+
binary_string(&sv)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Encode state as update (optionally diffed against a state vector)
|
|
423
|
+
fn encode_state_as_update(&self, args: &[Value]) -> Result<RString, Error> {
|
|
424
|
+
let sv_bytes: Option<Vec<u8>> = if args.is_empty() {
|
|
425
|
+
None
|
|
426
|
+
} else {
|
|
427
|
+
let sv_string: RString = TryConvert::try_convert(args[0])?;
|
|
428
|
+
Some(copy_bytes(sv_string))
|
|
429
|
+
};
|
|
430
|
+
let awareness = &self.0;
|
|
431
|
+
let update = nogvl(move || -> Result<Vec<u8>, String> {
|
|
432
|
+
let sv = match &sv_bytes {
|
|
433
|
+
None => yrs::StateVector::default(),
|
|
434
|
+
Some(bytes) => yrs::StateVector::decode_v1(bytes).map_err(|e| e.to_string())?,
|
|
435
|
+
};
|
|
436
|
+
let txn = awareness.doc().transact();
|
|
437
|
+
Ok(txn.encode_state_as_update_v1(&sv))
|
|
438
|
+
})
|
|
439
|
+
.map_err(runtime_error)?;
|
|
440
|
+
Ok(binary_string(&update))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// Set local awareness state (JSON string)
|
|
444
|
+
fn set_local_state(&self, json: String) -> Result<(), Error> {
|
|
445
|
+
let awareness = &self.0;
|
|
446
|
+
let value: serde_json::Value =
|
|
447
|
+
serde_json::from_str(&json).map_err(|e| runtime_error(e.to_string()))?;
|
|
448
|
+
awareness
|
|
449
|
+
.set_local_state(value)
|
|
450
|
+
.map_err(|e| runtime_error(e.to_string()))?;
|
|
451
|
+
Ok(())
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// Get local awareness state as JSON string (or nil if not set)
|
|
455
|
+
fn local_state(&self) -> Option<String> {
|
|
456
|
+
let awareness = &self.0;
|
|
457
|
+
awareness
|
|
458
|
+
.local_state::<serde_json::Value>()
|
|
459
|
+
.map(|v| v.to_string())
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/// Clear local awareness state
|
|
463
|
+
fn clear_local_state(&self) {
|
|
464
|
+
let awareness = &self.0;
|
|
465
|
+
awareness.clean_local_state();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// Get awareness update for broadcasting to peers
|
|
469
|
+
fn encode_awareness_update(&self) -> Result<RString, Error> {
|
|
470
|
+
let awareness = &self.0;
|
|
471
|
+
let update = awareness
|
|
472
|
+
.update()
|
|
473
|
+
.map_err(|e| runtime_error(e.to_string()))?;
|
|
474
|
+
let msg = Message::Awareness(update);
|
|
475
|
+
Ok(binary_string(&msg.encode_v1()))
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Apply a raw update to the underlying document
|
|
479
|
+
fn apply_update(&self, update: RString) -> Result<(), Error> {
|
|
480
|
+
let update_bytes = copy_bytes(update);
|
|
481
|
+
let awareness = &self.0;
|
|
482
|
+
nogvl(move || -> Result<(), String> {
|
|
483
|
+
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
484
|
+
let mut txn = awareness.doc().transact_mut();
|
|
485
|
+
txn.apply_update(update).map_err(|e| e.to_string())
|
|
486
|
+
})
|
|
487
|
+
.map_err(runtime_error)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// Decode the awareness client IDs referenced by a protocol message
|
|
491
|
+
/// (which may pack several sub-messages together). Sync sub-messages are
|
|
492
|
+
/// ignored. The ActionCable layer uses this to learn which presence
|
|
493
|
+
/// states arrived on a connection, so it can clear them when that
|
|
494
|
+
/// connection closes.
|
|
495
|
+
fn awareness_client_ids(&self, data: RString) -> Result<Vec<u64>, Error> {
|
|
496
|
+
let data_bytes = copy_bytes(data);
|
|
497
|
+
nogvl(move || awareness_client_ids_in(&data_bytes)).map_err(runtime_error)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/// Classify a frame for safe routing and relay. Returns a code only when
|
|
501
|
+
/// the frame is exactly one well-formed message that consumes the whole
|
|
502
|
+
/// buffer, so a malformed, truncated, multi-message, or trailing-garbage
|
|
503
|
+
/// frame (which a malicious client could craft to disrupt others if
|
|
504
|
+
/// relayed) is rejected up front:
|
|
505
|
+
/// 0 = drop (malformed, multiple, unknown, or empty)
|
|
506
|
+
/// 1 = sync step1 (a request: respond, do not relay)
|
|
507
|
+
/// 2 = sync step2/update (a document change: record/apply/relay)
|
|
508
|
+
/// 3 = awareness (presence: relay)
|
|
509
|
+
/// 4 = awareness query (a request: respond, do not relay)
|
|
510
|
+
fn message_kind(&self, data: RString) -> u8 {
|
|
511
|
+
let data_bytes = copy_bytes(data);
|
|
512
|
+
nogvl(move || classify_message(&data_bytes))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// Extract the document-update delta carried by a protocol message: the
|
|
516
|
+
/// payloads of any Update or SyncStep2 sub-messages, merged into a single
|
|
517
|
+
/// update. Returns nil if the message carries no document change (for
|
|
518
|
+
/// instance a SyncStep1 request or an awareness update). The strict audit
|
|
519
|
+
/// path records this exact delta before applying it.
|
|
520
|
+
fn update_from_message(&self, data: RString) -> Result<Option<RString>, Error> {
|
|
521
|
+
let data_bytes = copy_bytes(data);
|
|
522
|
+
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(runtime_error)?;
|
|
523
|
+
Ok(merged.map(|b| binary_string(&b)))
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/// Mark the given clients as disconnected and return an awareness protocol
|
|
527
|
+
/// message (null-state, bumped clock) announcing their removal to peers.
|
|
528
|
+
/// Only clients currently known to this Awareness are removed; unknown
|
|
529
|
+
/// IDs are skipped (so we never broadcast phantom removals). Returns an
|
|
530
|
+
/// empty string when nothing was removed.
|
|
531
|
+
fn remove_clients(&self, client_ids: Vec<u64>) -> Result<RString, Error> {
|
|
532
|
+
let awareness = &self.0;
|
|
533
|
+
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
534
|
+
let mut removed = Vec::new();
|
|
535
|
+
for id in client_ids {
|
|
536
|
+
if awareness.meta(id).is_some() {
|
|
537
|
+
awareness.remove_state(id);
|
|
538
|
+
removed.push(id);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if removed.is_empty() {
|
|
542
|
+
return Ok(Vec::new());
|
|
543
|
+
}
|
|
544
|
+
let update = awareness
|
|
545
|
+
.update_with_clients(removed)
|
|
546
|
+
.map_err(|e| e.to_string())?;
|
|
547
|
+
Ok(Message::Awareness(update).encode_v1())
|
|
548
|
+
})
|
|
549
|
+
.map_err(runtime_error)?;
|
|
550
|
+
Ok(binary_string(&encoded))
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// Module Initialization
|
|
556
|
+
// ============================================================================
|
|
557
|
+
|
|
558
|
+
#[magnus::init]
|
|
559
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
560
|
+
let module = ruby.define_module("YrbLite")?;
|
|
561
|
+
|
|
562
|
+
// Define error class
|
|
563
|
+
let standard_error: magnus::RClass = ruby.eval("StandardError")?;
|
|
564
|
+
let _error_class = module.define_class("Error", standard_error)?;
|
|
565
|
+
|
|
566
|
+
// Define Doc class
|
|
567
|
+
let doc_class = module.define_class("Doc", ruby.class_object())?;
|
|
568
|
+
doc_class.define_singleton_method("new", function!(RbDoc::new, -1))?;
|
|
569
|
+
doc_class.define_method("client_id", method!(RbDoc::client_id, 0))?;
|
|
570
|
+
doc_class.define_method("guid", method!(RbDoc::guid, 0))?;
|
|
571
|
+
doc_class.define_method(
|
|
572
|
+
"encode_state_vector",
|
|
573
|
+
method!(RbDoc::encode_state_vector, 0),
|
|
574
|
+
)?;
|
|
575
|
+
doc_class.define_method(
|
|
576
|
+
"encode_state_as_update",
|
|
577
|
+
method!(RbDoc::encode_state_as_update, -1),
|
|
578
|
+
)?;
|
|
579
|
+
doc_class.define_method("apply_update", method!(RbDoc::apply_update, 1))?;
|
|
580
|
+
doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
|
|
581
|
+
doc_class.define_method("sync_step2", method!(RbDoc::sync_step2, 1))?;
|
|
582
|
+
doc_class.define_method(
|
|
583
|
+
"handle_sync_message",
|
|
584
|
+
method!(RbDoc::handle_sync_message, 1),
|
|
585
|
+
)?;
|
|
586
|
+
doc_class.define_method(
|
|
587
|
+
"encode_update_message",
|
|
588
|
+
method!(RbDoc::encode_update_message, 1),
|
|
589
|
+
)?;
|
|
590
|
+
|
|
591
|
+
// Define Awareness class
|
|
592
|
+
let awareness_class = module.define_class("Awareness", ruby.class_object())?;
|
|
593
|
+
awareness_class.define_singleton_method("new", function!(RbAwareness::new, -1))?;
|
|
594
|
+
awareness_class.define_method("client_id", method!(RbAwareness::client_id, 0))?;
|
|
595
|
+
awareness_class.define_method("guid", method!(RbAwareness::guid, 0))?;
|
|
596
|
+
awareness_class.define_method("start", method!(RbAwareness::start, 0))?;
|
|
597
|
+
awareness_class.define_method("sync_step1", method!(RbAwareness::sync_step1, 0))?;
|
|
598
|
+
awareness_class.define_method("handle", method!(RbAwareness::handle, 1))?;
|
|
599
|
+
awareness_class.define_method("encode_update", method!(RbAwareness::encode_update, 1))?;
|
|
600
|
+
awareness_class.define_method(
|
|
601
|
+
"encode_state_vector",
|
|
602
|
+
method!(RbAwareness::encode_state_vector, 0),
|
|
603
|
+
)?;
|
|
604
|
+
awareness_class.define_method(
|
|
605
|
+
"encode_state_as_update",
|
|
606
|
+
method!(RbAwareness::encode_state_as_update, -1),
|
|
607
|
+
)?;
|
|
608
|
+
awareness_class.define_method("apply_update", method!(RbAwareness::apply_update, 1))?;
|
|
609
|
+
awareness_class.define_method("set_local_state", method!(RbAwareness::set_local_state, 1))?;
|
|
610
|
+
awareness_class.define_method("local_state", method!(RbAwareness::local_state, 0))?;
|
|
611
|
+
awareness_class.define_method(
|
|
612
|
+
"clear_local_state",
|
|
613
|
+
method!(RbAwareness::clear_local_state, 0),
|
|
614
|
+
)?;
|
|
615
|
+
awareness_class.define_method(
|
|
616
|
+
"encode_awareness_update",
|
|
617
|
+
method!(RbAwareness::encode_awareness_update, 0),
|
|
618
|
+
)?;
|
|
619
|
+
awareness_class.define_method(
|
|
620
|
+
"awareness_client_ids",
|
|
621
|
+
method!(RbAwareness::awareness_client_ids, 1),
|
|
622
|
+
)?;
|
|
623
|
+
awareness_class.define_method("remove_clients", method!(RbAwareness::remove_clients, 1))?;
|
|
624
|
+
awareness_class.define_method(
|
|
625
|
+
"update_from_message",
|
|
626
|
+
method!(RbAwareness::update_from_message, 1),
|
|
627
|
+
)?;
|
|
628
|
+
awareness_class.define_method("message_kind", method!(RbAwareness::message_kind, 1))?;
|
|
629
|
+
|
|
630
|
+
// Define message type constants
|
|
631
|
+
module.const_set("MSG_SYNC", 0u8)?;
|
|
632
|
+
module.const_set("MSG_AWARENESS", 1u8)?;
|
|
633
|
+
module.const_set("MSG_AUTH", 2u8)?;
|
|
634
|
+
module.const_set("MSG_QUERY_AWARENESS", 3u8)?;
|
|
635
|
+
module.const_set("MSG_SYNC_STEP1", 0u8)?;
|
|
636
|
+
module.const_set("MSG_SYNC_STEP2", 1u8)?;
|
|
637
|
+
module.const_set("MSG_SYNC_UPDATE", 2u8)?;
|
|
638
|
+
|
|
639
|
+
Ok(())
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Tests for the pure protocol helpers (run with `cargo test`, no Ruby VM)
|
|
644
|
+
// ============================================================================
|
|
645
|
+
|
|
646
|
+
#[cfg(test)]
|
|
647
|
+
mod tests {
|
|
648
|
+
use super::*;
|
|
649
|
+
use yrs::sync::Awareness;
|
|
650
|
+
use yrs::Text;
|
|
651
|
+
|
|
652
|
+
fn text_update(content: &str) -> Vec<u8> {
|
|
653
|
+
let doc = Doc::new();
|
|
654
|
+
let text = doc.get_or_insert_text("content");
|
|
655
|
+
text.insert(&mut doc.transact_mut(), 0, content);
|
|
656
|
+
let update = doc
|
|
657
|
+
.transact()
|
|
658
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
659
|
+
update
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
fn update_frame(content: &str) -> Vec<u8> {
|
|
663
|
+
Message::Sync(SyncMessage::Update(text_update(content))).encode_v1()
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
fn step1_frame() -> Vec<u8> {
|
|
667
|
+
Message::Sync(SyncMessage::SyncStep1(yrs::StateVector::default())).encode_v1()
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fn awareness_frame(client_id: u64) -> Vec<u8> {
|
|
671
|
+
let awareness = Awareness::new(Doc::with_client_id(client_id));
|
|
672
|
+
awareness
|
|
673
|
+
.set_local_state(serde_json::json!({ "user": "alice" }))
|
|
674
|
+
.unwrap();
|
|
675
|
+
Message::Awareness(awareness.update().unwrap()).encode_v1()
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
#[test]
|
|
679
|
+
fn classify_accepts_clean_single_messages() {
|
|
680
|
+
assert_eq!(classify_message(&step1_frame()), 1);
|
|
681
|
+
assert_eq!(classify_message(&update_frame("hi")), 2);
|
|
682
|
+
assert_eq!(classify_message(&awareness_frame(7)), 3);
|
|
683
|
+
assert_eq!(classify_message(&Message::AwarenessQuery.encode_v1()), 4);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
#[test]
|
|
687
|
+
fn classify_rejects_unsafe_frames() {
|
|
688
|
+
assert_eq!(classify_message(b""), 0, "empty");
|
|
689
|
+
assert_eq!(classify_message(&[0xff, 0xff, 0xff]), 0, "garbage");
|
|
690
|
+
assert_eq!(classify_message(&[0x63, 0x63, 0x63]), 0, "unknown type");
|
|
691
|
+
|
|
692
|
+
let mut two = update_frame("a");
|
|
693
|
+
two.extend(awareness_frame(1)); // two messages packed together
|
|
694
|
+
assert_eq!(classify_message(&two), 0, "multi-message");
|
|
695
|
+
|
|
696
|
+
let mut trailing = update_frame("a");
|
|
697
|
+
trailing.extend_from_slice(&[0xde, 0xad]);
|
|
698
|
+
assert_eq!(classify_message(&trailing), 0, "trailing garbage");
|
|
699
|
+
|
|
700
|
+
let frame = update_frame("hello");
|
|
701
|
+
assert_eq!(classify_message(&frame[..frame.len() / 2]), 0, "truncated");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#[test]
|
|
705
|
+
fn merged_doc_update_extracts_and_skips_no_ops() {
|
|
706
|
+
// A document update yields a delta that reconstructs the content.
|
|
707
|
+
let delta = merged_doc_update(&update_frame("hello"))
|
|
708
|
+
.unwrap()
|
|
709
|
+
.expect("a document update");
|
|
710
|
+
let doc = Doc::new();
|
|
711
|
+
doc.transact_mut()
|
|
712
|
+
.apply_update(yrs::Update::decode_v1(&delta).unwrap())
|
|
713
|
+
.unwrap();
|
|
714
|
+
// The delta carried real content, so applying it advances the doc.
|
|
715
|
+
assert!(!doc.transact().state_vector().is_empty());
|
|
716
|
+
|
|
717
|
+
// A SyncStep1 request carries no document change.
|
|
718
|
+
assert!(merged_doc_update(&step1_frame()).unwrap().is_none());
|
|
719
|
+
|
|
720
|
+
// An empty SyncStep2 (no new structs) is a no-op.
|
|
721
|
+
let empty = Message::Sync(SyncMessage::SyncStep2(
|
|
722
|
+
Doc::new()
|
|
723
|
+
.transact()
|
|
724
|
+
.encode_state_as_update_v1(&yrs::StateVector::default()),
|
|
725
|
+
))
|
|
726
|
+
.encode_v1();
|
|
727
|
+
assert!(merged_doc_update(&empty).unwrap().is_none());
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#[test]
|
|
731
|
+
fn merged_doc_update_merges_multiple_updates() {
|
|
732
|
+
// Two updates from different clients packed in one frame merge into one.
|
|
733
|
+
let mut frame = update_frame("a");
|
|
734
|
+
frame.extend(update_frame("b"));
|
|
735
|
+
let merged = merged_doc_update(&frame).unwrap().expect("merged update");
|
|
736
|
+
|
|
737
|
+
// The merged update must decode cleanly as a single update.
|
|
738
|
+
assert!(yrs::Update::decode_v1(&merged).is_ok());
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
#[test]
|
|
742
|
+
fn awareness_client_ids_are_collected() {
|
|
743
|
+
assert_eq!(
|
|
744
|
+
awareness_client_ids_in(&awareness_frame(111)).unwrap(),
|
|
745
|
+
vec![111]
|
|
746
|
+
);
|
|
747
|
+
// A document frame has no awareness client ids.
|
|
748
|
+
assert!(awareness_client_ids_in(&update_frame("x"))
|
|
749
|
+
.unwrap()
|
|
750
|
+
.is_empty());
|
|
751
|
+
}
|
|
752
|
+
}
|