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.
@@ -1,12 +1,19 @@
1
- use magnus::{function, method, prelude::*, Error, RString, Ruby, TryConvert, Value};
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::{Decode, DecoderV1};
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
- fn runtime_error(msg: String) -> Error {
127
- Error::new(Ruby::get().unwrap().exception_runtime_error(), msg)
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
- // Pure protocol helpers (no Ruby, no GVL); unit-tested in the `tests` module.
132
- // ============================================================================
133
-
134
- /// Classify a frame: a non-zero code only for exactly one well-formed message
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
- /// Merge the document-update deltas (Update / SyncStep2 payloads) carried by a
156
- /// frame into one update, or `None` if the frame carries no document change
157
- /// (a request, an awareness update, or a no-op handshake SyncStep2).
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
- /// Collect the awareness client IDs referenced by a frame's awareness messages.
191
- fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
192
- let mut decoder = DecoderV1::new(Cursor::new(bytes));
193
- let mut ids = Vec::new();
194
- for msg in MessageReader::new(&mut decoder) {
195
- if let Message::Awareness(update) = msg.map_err(|e| e.to_string())? {
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(ids)
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: u64 = TryConvert::try_convert(args[0])?;
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(runtime_error)?;
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(runtime_error)
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(runtime_error)
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(runtime_error)?;
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(runtime_error)?;
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 = unsafe { update.as_slice() };
380
- let msg = Message::Sync(SyncMessage::Update(update_bytes.to_vec()));
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: u64 = TryConvert::try_convert(args[0])?;
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(runtime_error)?;
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(runtime_error)?;
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 = unsafe { update.as_slice() };
474
- let msg = Message::Sync(SyncMessage::Update(update_bytes.to_vec()));
475
- binary_string(&msg.encode_v1())
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(runtime_error)?;
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| runtime_error(e.to_string()))?;
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(runtime_error)
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(runtime_error)?;
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(runtime_error)
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(runtime_error)
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(runtime_error)
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(runtime_error)?;
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(runtime_error)?;
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
- }