yrb-lite 0.1.0.beta5 → 0.1.0.beta6
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 +4 -4
- data/CHANGELOG.md +38 -16
- data/README.md +100 -146
- data/ext/yrb_lite/src/lib.rs +97 -272
- data/ext/yrb_lite/src/protocol.rs +642 -0
- data/lib/yrb_lite/version.rb +1 -1
- metadata +2 -1
data/ext/yrb_lite/src/lib.rs
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
use magnus::{
|
|
1
|
+
use magnus::{
|
|
2
|
+
function, method, prelude::*, Error, ExceptionClass, RString, Ruby, TryConvert, Value,
|
|
3
|
+
};
|
|
2
4
|
use std::sync::Mutex;
|
|
3
|
-
use yrs::encoding::read::{Cursor, Read};
|
|
4
|
-
use yrs::sync::protocol::MessageReader;
|
|
5
5
|
use yrs::sync::{Awareness, DefaultProtocol, Message, Protocol, SyncMessage};
|
|
6
|
-
use yrs::updates::decoder::
|
|
6
|
+
use yrs::updates::decoder::Decode;
|
|
7
7
|
use yrs::updates::encoder::{Encode, Encoder, EncoderV1};
|
|
8
8
|
use yrs::{ClientID, Doc, ReadTxn, Transact};
|
|
9
9
|
|
|
10
|
+
mod protocol;
|
|
11
|
+
use protocol::{
|
|
12
|
+
awareness_client_ids_in, classify_message, doc_has_pending, merged_doc_update,
|
|
13
|
+
update_advances_doc, update_is_ready, validate_frame_client_ids,
|
|
14
|
+
validate_state_vector_client_ids, validate_update_client_ids,
|
|
15
|
+
};
|
|
16
|
+
|
|
10
17
|
/// Wrapper around yrs Doc.
|
|
11
18
|
///
|
|
12
19
|
/// Thread safety: `yrs::Doc` is `Send + Sync`. Its `transact()`/`transact_mut()`
|
|
@@ -123,98 +130,37 @@ fn copy_bytes(s: RString) -> Vec<u8> {
|
|
|
123
130
|
unsafe { s.as_slice() }.to_vec()
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
/// Build a `YrbLite::Error` (the gem's own error class, defined in `init`) so
|
|
134
|
+
/// native decode/apply/validation failures surface as a project-specific error
|
|
135
|
+
/// rather than a generic RuntimeError. Falls back to RuntimeError only if the
|
|
136
|
+
/// class somehow can't be resolved.
|
|
137
|
+
fn yrb_error(msg: String) -> Error {
|
|
138
|
+
let ruby = Ruby::get().unwrap();
|
|
139
|
+
let class = ruby
|
|
140
|
+
.eval::<ExceptionClass>("YrbLite::Error")
|
|
141
|
+
.unwrap_or_else(|_| ruby.exception_runtime_error());
|
|
142
|
+
Error::new(class, msg)
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/// that consumes the whole buffer (see `RbAwareness::message_kind` for codes).
|
|
136
|
-
fn classify_message(bytes: &[u8]) -> u8 {
|
|
137
|
-
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
138
|
-
let msg = match Message::decode(&mut decoder) {
|
|
139
|
-
Ok(msg) => msg,
|
|
140
|
-
Err(_) => return 0, // empty or malformed
|
|
141
|
-
};
|
|
142
|
-
// Any remaining byte means a second message or trailing garbage.
|
|
143
|
-
if decoder.read_u8().is_ok() {
|
|
144
|
-
return 0;
|
|
145
|
-
}
|
|
146
|
-
match msg {
|
|
147
|
-
Message::Sync(SyncMessage::SyncStep1(_)) => 1,
|
|
148
|
-
Message::Sync(SyncMessage::SyncStep2(_)) | Message::Sync(SyncMessage::Update(_)) => 2,
|
|
149
|
-
Message::Awareness(_) => 3,
|
|
150
|
-
Message::AwarenessQuery => 4,
|
|
151
|
-
_ => 0, // Auth / Custom: not part of our model
|
|
152
|
-
}
|
|
153
|
-
}
|
|
145
|
+
/// Yjs/lib0 client IDs must be JS-safe integers (<= 2^53 - 1). Above that they
|
|
146
|
+
/// round or collide when crossing the JS/Yjs boundary, and a client-id collision
|
|
147
|
+
/// corrupts a CRDT. Explicit (Ruby-supplied) IDs are validated here; the random
|
|
148
|
+
/// default IDs that yrs generates are already in range.
|
|
149
|
+
const MAX_SAFE_CLIENT_ID: u64 = (1 << 53) - 1;
|
|
154
150
|
|
|
155
|
-
///
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
|
|
159
|
-
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
160
|
-
let mut updates: Vec<Vec<u8>> = Vec::new();
|
|
161
|
-
for msg in MessageReader::new(&mut decoder) {
|
|
162
|
-
match msg.map_err(|e| e.to_string())? {
|
|
163
|
-
Message::Sync(SyncMessage::Update(u)) | Message::Sync(SyncMessage::SyncStep2(u)) => {
|
|
164
|
-
updates.push(u)
|
|
165
|
-
}
|
|
166
|
-
_ => {}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
let merged = match updates.len() {
|
|
170
|
-
0 => return Ok(None),
|
|
171
|
-
1 => updates.pop().unwrap(),
|
|
172
|
-
_ => yrs::merge_updates_v1(&updates).map_err(|e| e.to_string())?,
|
|
173
|
-
};
|
|
174
|
-
let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
|
|
175
|
-
// A genuine no-op (e.g. the empty SyncStep2 in an opening handshake) carries
|
|
176
|
-
// no structs, no deletes, and no dependencies. We must NOT treat a causally-
|
|
177
|
-
// pending update as a no-op: since yrs 0.26 such an update reports an empty
|
|
178
|
-
// state_vector (its structs can't integrate yet), but it still carries
|
|
179
|
-
// content and a non-empty lower bound (the deps it's waiting on). Dropping it
|
|
180
|
-
// here would silently swallow a gappy update instead of rejecting + resyncing.
|
|
181
|
-
if update.state_vector().is_empty()
|
|
182
|
-
&& update.delete_set().is_empty()
|
|
183
|
-
&& update.state_vector_lower().is_empty()
|
|
184
|
-
{
|
|
185
|
-
return Ok(None);
|
|
186
|
-
}
|
|
187
|
-
Ok(Some(merged))
|
|
151
|
+
/// Pure predicate (no Ruby), so the boundary is unit-testable without a VM.
|
|
152
|
+
pub(crate) fn is_safe_client_id(id: u64) -> bool {
|
|
153
|
+
id <= MAX_SAFE_CLIENT_ID
|
|
188
154
|
}
|
|
189
155
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
ids.extend(update.clients.keys().map(|c| c.get()));
|
|
197
|
-
}
|
|
156
|
+
fn validate_client_id(id: u64) -> Result<u64, Error> {
|
|
157
|
+
if !is_safe_client_id(id) {
|
|
158
|
+
return Err(yrb_error(format!(
|
|
159
|
+
"client_id {id} exceeds the maximum safe integer ({MAX_SAFE_CLIENT_ID} = 2^53 - 1); \
|
|
160
|
+
Yjs client IDs must be JS-safe integers to avoid collisions"
|
|
161
|
+
)));
|
|
198
162
|
}
|
|
199
|
-
Ok(
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/// True if applying `update_bytes` to `doc` would integrate cleanly: every
|
|
203
|
-
/// dependency the update references is already present (the doc's state vector
|
|
204
|
-
/// covers the update's lower bound). A pure read; does not mutate the doc.
|
|
205
|
-
/// When false, applying it would park a pending struct -- the signal that an
|
|
206
|
-
/// earlier, causally-prior update is missing.
|
|
207
|
-
fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
|
|
208
|
-
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
209
|
-
Ok(doc.transact().state_vector() >= update.state_vector_lower())
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/// True if the doc holds pending structs or a pending delete set -- blocks that
|
|
213
|
-
/// couldn't integrate because a dependency is missing. Used as a backstop after
|
|
214
|
-
/// loading from storage: leftover pending means the stored log has a causal gap.
|
|
215
|
-
fn doc_has_pending(doc: &Doc) -> bool {
|
|
216
|
-
let txn = doc.transact();
|
|
217
|
-
txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
|
|
163
|
+
Ok(id)
|
|
218
164
|
}
|
|
219
165
|
|
|
220
166
|
// ============================================================================
|
|
@@ -227,7 +173,7 @@ impl RbDoc {
|
|
|
227
173
|
let doc = if args.is_empty() {
|
|
228
174
|
Doc::new()
|
|
229
175
|
} else {
|
|
230
|
-
let client_id
|
|
176
|
+
let client_id = validate_client_id(TryConvert::try_convert(args[0])?)?;
|
|
231
177
|
Doc::with_client_id(client_id)
|
|
232
178
|
};
|
|
233
179
|
Ok(RbDoc(doc))
|
|
@@ -270,7 +216,7 @@ impl RbDoc {
|
|
|
270
216
|
let txn = doc.transact();
|
|
271
217
|
Ok(txn.encode_state_as_update_v1(&sv))
|
|
272
218
|
})
|
|
273
|
-
.map_err(
|
|
219
|
+
.map_err(yrb_error)?;
|
|
274
220
|
Ok(binary_string(&update))
|
|
275
221
|
}
|
|
276
222
|
|
|
@@ -279,11 +225,12 @@ impl RbDoc {
|
|
|
279
225
|
let update_bytes = copy_bytes(update);
|
|
280
226
|
let doc = &self.0;
|
|
281
227
|
nogvl(move || -> Result<(), String> {
|
|
228
|
+
validate_update_client_ids(&update_bytes)?;
|
|
282
229
|
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
283
230
|
let mut txn = doc.transact_mut();
|
|
284
231
|
txn.apply_update(update).map_err(|e| e.to_string())
|
|
285
232
|
})
|
|
286
|
-
.map_err(
|
|
233
|
+
.map_err(yrb_error)
|
|
287
234
|
}
|
|
288
235
|
|
|
289
236
|
/// True if applying `update` would integrate cleanly (its dependencies are
|
|
@@ -292,7 +239,16 @@ impl RbDoc {
|
|
|
292
239
|
fn update_ready(&self, update: RString) -> Result<bool, Error> {
|
|
293
240
|
let update_bytes = copy_bytes(update);
|
|
294
241
|
let doc = &self.0;
|
|
295
|
-
nogvl(move || update_is_ready(doc, &update_bytes)).map_err(
|
|
242
|
+
nogvl(move || update_is_ready(doc, &update_bytes)).map_err(yrb_error)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// True if applying `update` would change the document (it carries new
|
|
246
|
+
/// content), false if the doc already contains it (an already-applied
|
|
247
|
+
/// retry). See `update_advances_doc`. Pure read; does not mutate.
|
|
248
|
+
fn update_advances(&self, update: RString) -> Result<bool, Error> {
|
|
249
|
+
let update_bytes = copy_bytes(update);
|
|
250
|
+
let doc = &self.0;
|
|
251
|
+
nogvl(move || update_advances_doc(doc, &update_bytes)).map_err(yrb_error)
|
|
296
252
|
}
|
|
297
253
|
|
|
298
254
|
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
@@ -318,12 +274,13 @@ impl RbDoc {
|
|
|
318
274
|
let sv_data = copy_bytes(sv_bytes);
|
|
319
275
|
let doc = &self.0;
|
|
320
276
|
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
277
|
+
validate_state_vector_client_ids(&sv_data)?;
|
|
321
278
|
let sv = yrs::StateVector::decode_v1(&sv_data).map_err(|e| e.to_string())?;
|
|
322
279
|
let txn = doc.transact();
|
|
323
280
|
let update = txn.encode_state_as_update_v1(&sv);
|
|
324
281
|
Ok(Message::Sync(SyncMessage::SyncStep2(update)).encode_v1())
|
|
325
282
|
})
|
|
326
|
-
.map_err(
|
|
283
|
+
.map_err(yrb_error)?;
|
|
327
284
|
Ok(binary_string(&encoded))
|
|
328
285
|
}
|
|
329
286
|
|
|
@@ -335,6 +292,7 @@ impl RbDoc {
|
|
|
335
292
|
|
|
336
293
|
let (msg_type, sync_type, response) =
|
|
337
294
|
nogvl(move || -> Result<(u8, u8, Vec<u8>), String> {
|
|
295
|
+
validate_frame_client_ids(&data_bytes)?;
|
|
338
296
|
let msg = Message::decode_v1(&data_bytes).map_err(|e| e.to_string())?;
|
|
339
297
|
|
|
340
298
|
match msg {
|
|
@@ -369,15 +327,15 @@ impl RbDoc {
|
|
|
369
327
|
Message::Custom(tag, _) => Ok((tag, 0, Vec::new())),
|
|
370
328
|
}
|
|
371
329
|
})
|
|
372
|
-
.map_err(
|
|
330
|
+
.map_err(yrb_error)?;
|
|
373
331
|
|
|
374
332
|
Ok(Some((msg_type, sync_type, binary_string(&response))))
|
|
375
333
|
}
|
|
376
334
|
|
|
377
335
|
/// Encode raw update bytes as a sync Update message
|
|
378
336
|
fn encode_update_message(&self, update: RString) -> RString {
|
|
379
|
-
let update_bytes =
|
|
380
|
-
let msg = Message::Sync(SyncMessage::Update(update_bytes
|
|
337
|
+
let update_bytes = copy_bytes(update);
|
|
338
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
381
339
|
binary_string(&msg.encode_v1())
|
|
382
340
|
}
|
|
383
341
|
}
|
|
@@ -392,7 +350,7 @@ impl RbAwareness {
|
|
|
392
350
|
let awareness = if args.is_empty() {
|
|
393
351
|
Awareness::new(Doc::new())
|
|
394
352
|
} else {
|
|
395
|
-
let client_id
|
|
353
|
+
let client_id = validate_client_id(TryConvert::try_convert(args[0])?)?;
|
|
396
354
|
Awareness::new(Doc::with_client_id(client_id))
|
|
397
355
|
};
|
|
398
356
|
Ok(RbAwareness(Mutex::new(awareness)))
|
|
@@ -437,7 +395,7 @@ impl RbAwareness {
|
|
|
437
395
|
.map_err(|e| e.to_string())?;
|
|
438
396
|
Ok(encoder.to_vec())
|
|
439
397
|
})
|
|
440
|
-
.map_err(
|
|
398
|
+
.map_err(yrb_error)?;
|
|
441
399
|
Ok(binary_string(&encoded))
|
|
442
400
|
}
|
|
443
401
|
|
|
@@ -448,6 +406,7 @@ impl RbAwareness {
|
|
|
448
406
|
let awareness = &self.0;
|
|
449
407
|
|
|
450
408
|
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
409
|
+
validate_frame_client_ids(&data_bytes)?;
|
|
451
410
|
let mut awareness = awareness.lock().unwrap();
|
|
452
411
|
let protocol = DefaultProtocol;
|
|
453
412
|
let responses = protocol
|
|
@@ -464,15 +423,20 @@ impl RbAwareness {
|
|
|
464
423
|
}
|
|
465
424
|
Ok(encoder.to_vec())
|
|
466
425
|
})
|
|
467
|
-
.map_err(
|
|
426
|
+
.map_err(yrb_error)?;
|
|
468
427
|
Ok(binary_string(&encoded))
|
|
469
428
|
}
|
|
470
429
|
|
|
471
430
|
/// Encode an update message for broadcasting changes to peers.
|
|
472
|
-
fn encode_update(&self, update: RString) -> RString {
|
|
473
|
-
let update_bytes =
|
|
474
|
-
|
|
475
|
-
|
|
431
|
+
fn encode_update(&self, update: RString) -> Result<RString, Error> {
|
|
432
|
+
let update_bytes = copy_bytes(update);
|
|
433
|
+
nogvl({
|
|
434
|
+
let update_bytes = update_bytes.clone();
|
|
435
|
+
move || validate_update_client_ids(&update_bytes)
|
|
436
|
+
})
|
|
437
|
+
.map_err(yrb_error)?;
|
|
438
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
439
|
+
Ok(binary_string(&msg.encode_v1()))
|
|
476
440
|
}
|
|
477
441
|
|
|
478
442
|
/// Get the current state vector encoded as bytes
|
|
@@ -504,14 +468,14 @@ impl RbAwareness {
|
|
|
504
468
|
let txn = doc.transact();
|
|
505
469
|
Ok(txn.encode_state_as_update_v1(&sv))
|
|
506
470
|
})
|
|
507
|
-
.map_err(
|
|
471
|
+
.map_err(yrb_error)?;
|
|
508
472
|
Ok(binary_string(&update))
|
|
509
473
|
}
|
|
510
474
|
|
|
511
475
|
/// Set local awareness state (JSON string)
|
|
512
476
|
fn set_local_state(&self, json: String) -> Result<(), Error> {
|
|
513
477
|
let value: serde_json::Value =
|
|
514
|
-
serde_json::from_str(&json).map_err(|e|
|
|
478
|
+
serde_json::from_str(&json).map_err(|e| yrb_error(e.to_string()))?;
|
|
515
479
|
let awareness = &self.0;
|
|
516
480
|
nogvl(move || -> Result<(), String> {
|
|
517
481
|
awareness
|
|
@@ -520,7 +484,7 @@ impl RbAwareness {
|
|
|
520
484
|
.set_local_state(value)
|
|
521
485
|
.map_err(|e| e.to_string())
|
|
522
486
|
})
|
|
523
|
-
.map_err(
|
|
487
|
+
.map_err(yrb_error)
|
|
524
488
|
}
|
|
525
489
|
|
|
526
490
|
/// Get local awareness state as JSON string (or nil if not set)
|
|
@@ -549,7 +513,7 @@ impl RbAwareness {
|
|
|
549
513
|
let update = awareness.update().map_err(|e| e.to_string())?;
|
|
550
514
|
Ok(Message::Awareness(update).encode_v1())
|
|
551
515
|
})
|
|
552
|
-
.map_err(
|
|
516
|
+
.map_err(yrb_error)?;
|
|
553
517
|
Ok(binary_string(&encoded))
|
|
554
518
|
}
|
|
555
519
|
|
|
@@ -558,12 +522,13 @@ impl RbAwareness {
|
|
|
558
522
|
let update_bytes = copy_bytes(update);
|
|
559
523
|
let awareness = &self.0;
|
|
560
524
|
nogvl(move || -> Result<(), String> {
|
|
525
|
+
validate_update_client_ids(&update_bytes)?;
|
|
561
526
|
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
562
527
|
let doc = awareness.lock().unwrap().doc().clone();
|
|
563
528
|
let mut txn = doc.transact_mut();
|
|
564
529
|
txn.apply_update(update).map_err(|e| e.to_string())
|
|
565
530
|
})
|
|
566
|
-
.map_err(
|
|
531
|
+
.map_err(yrb_error)
|
|
567
532
|
}
|
|
568
533
|
|
|
569
534
|
/// True if applying `update` would integrate cleanly (its dependencies are
|
|
@@ -576,7 +541,19 @@ impl RbAwareness {
|
|
|
576
541
|
let doc = awareness.lock().unwrap().doc().clone();
|
|
577
542
|
update_is_ready(&doc, &update_bytes)
|
|
578
543
|
})
|
|
579
|
-
.map_err(
|
|
544
|
+
.map_err(yrb_error)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// True if applying `update` would change the document, false if it's an
|
|
548
|
+
/// already-applied retry. See `update_advances_doc`. Pure read.
|
|
549
|
+
fn update_advances(&self, update: RString) -> Result<bool, Error> {
|
|
550
|
+
let update_bytes = copy_bytes(update);
|
|
551
|
+
let awareness = &self.0;
|
|
552
|
+
nogvl(move || {
|
|
553
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
554
|
+
update_advances_doc(&doc, &update_bytes)
|
|
555
|
+
})
|
|
556
|
+
.map_err(yrb_error)
|
|
580
557
|
}
|
|
581
558
|
|
|
582
559
|
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
@@ -596,7 +573,7 @@ impl RbAwareness {
|
|
|
596
573
|
/// connection closes.
|
|
597
574
|
fn awareness_client_ids(&self, data: RString) -> Result<Vec<u64>, Error> {
|
|
598
575
|
let data_bytes = copy_bytes(data);
|
|
599
|
-
nogvl(move || awareness_client_ids_in(&data_bytes)).map_err(
|
|
576
|
+
nogvl(move || awareness_client_ids_in(&data_bytes)).map_err(yrb_error)
|
|
600
577
|
}
|
|
601
578
|
|
|
602
579
|
/// Classify a frame for safe routing and relay. Returns a code only when
|
|
@@ -621,7 +598,7 @@ impl RbAwareness {
|
|
|
621
598
|
/// path records this exact delta before applying it.
|
|
622
599
|
fn update_from_message(&self, data: RString) -> Result<Option<RString>, Error> {
|
|
623
600
|
let data_bytes = copy_bytes(data);
|
|
624
|
-
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(
|
|
601
|
+
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(yrb_error)?;
|
|
625
602
|
Ok(merged.map(|b| binary_string(&b)))
|
|
626
603
|
}
|
|
627
604
|
|
|
@@ -631,6 +608,10 @@ impl RbAwareness {
|
|
|
631
608
|
/// IDs are skipped (so we never broadcast phantom removals). Returns an
|
|
632
609
|
/// empty string when nothing was removed.
|
|
633
610
|
fn remove_clients(&self, client_ids: Vec<u64>) -> Result<RString, Error> {
|
|
611
|
+
let client_ids = client_ids
|
|
612
|
+
.into_iter()
|
|
613
|
+
.map(validate_client_id)
|
|
614
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
634
615
|
let awareness = &self.0;
|
|
635
616
|
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
636
617
|
let mut awareness = awareness.lock().unwrap();
|
|
@@ -650,7 +631,7 @@ impl RbAwareness {
|
|
|
650
631
|
.map_err(|e| e.to_string())?;
|
|
651
632
|
Ok(Message::Awareness(update).encode_v1())
|
|
652
633
|
})
|
|
653
|
-
.map_err(
|
|
634
|
+
.map_err(yrb_error)?;
|
|
654
635
|
Ok(binary_string(&encoded))
|
|
655
636
|
}
|
|
656
637
|
}
|
|
@@ -682,6 +663,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
682
663
|
)?;
|
|
683
664
|
doc_class.define_method("apply_update", method!(RbDoc::apply_update, 1))?;
|
|
684
665
|
doc_class.define_method("update_ready?", method!(RbDoc::update_ready, 1))?;
|
|
666
|
+
doc_class.define_method("update_advances?", method!(RbDoc::update_advances, 1))?;
|
|
685
667
|
doc_class.define_method("pending?", method!(RbDoc::pending, 0))?;
|
|
686
668
|
doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
|
|
687
669
|
doc_class.define_method("sync_step2", method!(RbDoc::sync_step2, 1))?;
|
|
@@ -713,6 +695,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
713
695
|
)?;
|
|
714
696
|
awareness_class.define_method("apply_update", method!(RbAwareness::apply_update, 1))?;
|
|
715
697
|
awareness_class.define_method("update_ready?", method!(RbAwareness::update_ready, 1))?;
|
|
698
|
+
awareness_class.define_method("update_advances?", method!(RbAwareness::update_advances, 1))?;
|
|
716
699
|
awareness_class.define_method("pending?", method!(RbAwareness::pending, 0))?;
|
|
717
700
|
awareness_class.define_method("set_local_state", method!(RbAwareness::set_local_state, 1))?;
|
|
718
701
|
awareness_class.define_method("local_state", method!(RbAwareness::local_state, 0))?;
|
|
@@ -746,161 +729,3 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
746
729
|
|
|
747
730
|
Ok(())
|
|
748
731
|
}
|
|
749
|
-
|
|
750
|
-
// ============================================================================
|
|
751
|
-
// Tests for the pure protocol helpers (run with `cargo test`, no Ruby VM)
|
|
752
|
-
// ============================================================================
|
|
753
|
-
|
|
754
|
-
#[cfg(test)]
|
|
755
|
-
mod tests {
|
|
756
|
-
use super::*;
|
|
757
|
-
use yrs::sync::Awareness;
|
|
758
|
-
use yrs::Text;
|
|
759
|
-
|
|
760
|
-
fn text_update(content: &str) -> Vec<u8> {
|
|
761
|
-
let doc = Doc::new();
|
|
762
|
-
let text = doc.get_or_insert_text("content");
|
|
763
|
-
text.insert(&mut doc.transact_mut(), 0, content);
|
|
764
|
-
let update = doc
|
|
765
|
-
.transact()
|
|
766
|
-
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
767
|
-
update
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
fn update_frame(content: &str) -> Vec<u8> {
|
|
771
|
-
Message::Sync(SyncMessage::Update(text_update(content))).encode_v1()
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
fn step1_frame() -> Vec<u8> {
|
|
775
|
-
Message::Sync(SyncMessage::SyncStep1(yrs::StateVector::default())).encode_v1()
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
fn awareness_frame(client_id: u64) -> Vec<u8> {
|
|
779
|
-
let mut awareness = Awareness::new(Doc::with_client_id(client_id));
|
|
780
|
-
awareness
|
|
781
|
-
.set_local_state(serde_json::json!({ "user": "alice" }))
|
|
782
|
-
.unwrap();
|
|
783
|
-
Message::Awareness(awareness.update().unwrap()).encode_v1()
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
#[test]
|
|
787
|
-
fn classify_accepts_clean_single_messages() {
|
|
788
|
-
assert_eq!(classify_message(&step1_frame()), 1);
|
|
789
|
-
assert_eq!(classify_message(&update_frame("hi")), 2);
|
|
790
|
-
assert_eq!(classify_message(&awareness_frame(7)), 3);
|
|
791
|
-
assert_eq!(classify_message(&Message::AwarenessQuery.encode_v1()), 4);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
#[test]
|
|
795
|
-
fn classify_rejects_unsafe_frames() {
|
|
796
|
-
assert_eq!(classify_message(b""), 0, "empty");
|
|
797
|
-
assert_eq!(classify_message(&[0xff, 0xff, 0xff]), 0, "garbage");
|
|
798
|
-
assert_eq!(classify_message(&[0x63, 0x63, 0x63]), 0, "unknown type");
|
|
799
|
-
|
|
800
|
-
let mut two = update_frame("a");
|
|
801
|
-
two.extend(awareness_frame(1)); // two messages packed together
|
|
802
|
-
assert_eq!(classify_message(&two), 0, "multi-message");
|
|
803
|
-
|
|
804
|
-
let mut trailing = update_frame("a");
|
|
805
|
-
trailing.extend_from_slice(&[0xde, 0xad]);
|
|
806
|
-
assert_eq!(classify_message(&trailing), 0, "trailing garbage");
|
|
807
|
-
|
|
808
|
-
let frame = update_frame("hello");
|
|
809
|
-
assert_eq!(classify_message(&frame[..frame.len() / 2]), 0, "truncated");
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
#[test]
|
|
813
|
-
fn merged_doc_update_extracts_and_skips_no_ops() {
|
|
814
|
-
// A document update yields a delta that reconstructs the content.
|
|
815
|
-
let delta = merged_doc_update(&update_frame("hello"))
|
|
816
|
-
.unwrap()
|
|
817
|
-
.expect("a document update");
|
|
818
|
-
let doc = Doc::new();
|
|
819
|
-
doc.transact_mut()
|
|
820
|
-
.apply_update(yrs::Update::decode_v1(&delta).unwrap())
|
|
821
|
-
.unwrap();
|
|
822
|
-
// The delta carried real content, so applying it advances the doc.
|
|
823
|
-
assert!(!doc.transact().state_vector().is_empty());
|
|
824
|
-
|
|
825
|
-
// A SyncStep1 request carries no document change.
|
|
826
|
-
assert!(merged_doc_update(&step1_frame()).unwrap().is_none());
|
|
827
|
-
|
|
828
|
-
// An empty SyncStep2 (no new structs) is a no-op.
|
|
829
|
-
let empty = Message::Sync(SyncMessage::SyncStep2(
|
|
830
|
-
Doc::new()
|
|
831
|
-
.transact()
|
|
832
|
-
.encode_state_as_update_v1(&yrs::StateVector::default()),
|
|
833
|
-
))
|
|
834
|
-
.encode_v1();
|
|
835
|
-
assert!(merged_doc_update(&empty).unwrap().is_none());
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
#[test]
|
|
839
|
-
fn merged_doc_update_merges_multiple_updates() {
|
|
840
|
-
// Two updates from different clients packed in one frame merge into one.
|
|
841
|
-
let mut frame = update_frame("a");
|
|
842
|
-
frame.extend(update_frame("b"));
|
|
843
|
-
let merged = merged_doc_update(&frame).unwrap().expect("merged update");
|
|
844
|
-
|
|
845
|
-
// The merged update must decode cleanly as a single update.
|
|
846
|
-
assert!(yrs::Update::decode_v1(&merged).is_ok());
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
#[test]
|
|
850
|
-
fn awareness_client_ids_are_collected() {
|
|
851
|
-
assert_eq!(
|
|
852
|
-
awareness_client_ids_in(&awareness_frame(111)).unwrap(),
|
|
853
|
-
vec![111]
|
|
854
|
-
);
|
|
855
|
-
// A document frame has no awareness client ids.
|
|
856
|
-
assert!(awareness_client_ids_in(&update_frame("x"))
|
|
857
|
-
.unwrap()
|
|
858
|
-
.is_empty());
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
#[test]
|
|
862
|
-
fn update_readiness_and_pending_detect_a_causal_gap() {
|
|
863
|
-
// Three sequential single-char inserts from one client: A, then B, then
|
|
864
|
-
// C. Each delta depends on the previous, so C can't integrate without B.
|
|
865
|
-
let src = Doc::new();
|
|
866
|
-
let txt = src.get_or_insert_text("t");
|
|
867
|
-
let mut deltas: Vec<Vec<u8>> = Vec::new();
|
|
868
|
-
let mut prev = yrs::StateVector::default();
|
|
869
|
-
for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
|
|
870
|
-
txt.insert(&mut src.transact_mut(), i as u32, ch);
|
|
871
|
-
deltas.push(src.transact().encode_state_as_update_v1(&prev));
|
|
872
|
-
prev = src.transact().state_vector();
|
|
873
|
-
}
|
|
874
|
-
let (u1, u2, u3) = (&deltas[0], &deltas[1], &deltas[2]);
|
|
875
|
-
|
|
876
|
-
// A doc holding only u1 (u2 was lost in transit / its record failed):
|
|
877
|
-
let doc = Doc::new();
|
|
878
|
-
doc.transact_mut()
|
|
879
|
-
.apply_update(yrs::Update::decode_v1(u1).unwrap())
|
|
880
|
-
.unwrap();
|
|
881
|
-
assert!(update_is_ready(&doc, u1).unwrap(), "u1 has no missing deps");
|
|
882
|
-
assert!(
|
|
883
|
-
!update_is_ready(&doc, u3).unwrap(),
|
|
884
|
-
"u3 depends on the missing u2"
|
|
885
|
-
);
|
|
886
|
-
assert!(
|
|
887
|
-
!doc_has_pending(&doc),
|
|
888
|
-
"nothing pending until u3 is applied"
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
// Applying u3 anyway parks it as a pending struct.
|
|
892
|
-
doc.transact_mut()
|
|
893
|
-
.apply_update(yrs::Update::decode_v1(u3).unwrap())
|
|
894
|
-
.unwrap();
|
|
895
|
-
assert!(
|
|
896
|
-
doc_has_pending(&doc),
|
|
897
|
-
"u3 is pending: its parent u2 is missing"
|
|
898
|
-
);
|
|
899
|
-
|
|
900
|
-
// Once u2 arrives (via resync), u3 integrates and pending clears.
|
|
901
|
-
doc.transact_mut()
|
|
902
|
-
.apply_update(yrs::Update::decode_v1(u2).unwrap())
|
|
903
|
-
.unwrap();
|
|
904
|
-
assert!(!doc_has_pending(&doc), "u2 arrived; u3 integrated");
|
|
905
|
-
}
|
|
906
|
-
}
|