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.
@@ -0,0 +1,642 @@
1
+ // Pure protocol helpers: no Ruby, no GVL, no `unsafe`. Everything here operates
2
+ // on plain byte slices and `yrs` types, so it's unit-tested directly (see the
3
+ // `tests` module below) without a Ruby VM. The Ruby-facing wrappers in lib.rs
4
+ // copy bytes out of Ruby strings and call into these under `nogvl`.
5
+ use crate::is_safe_client_id;
6
+ use std::sync::Arc;
7
+ use yrs::encoding::read::{Cursor, Error as ReadError, Read};
8
+ use yrs::sync::protocol::{
9
+ MessageReader, MSG_AUTH, MSG_AWARENESS, MSG_QUERY_AWARENESS, MSG_SYNC, MSG_SYNC_STEP_1,
10
+ MSG_SYNC_STEP_2, MSG_SYNC_UPDATE, PERMISSION_DENIED,
11
+ };
12
+ use yrs::sync::{Message, SyncMessage};
13
+ use yrs::updates::decoder::{Decode, Decoder, DecoderV1};
14
+ use yrs::{Any, ClientID, Doc, ReadTxn, Transact, ID};
15
+
16
+ fn unsafe_client_id_error(id: u64) -> ReadError {
17
+ ReadError::Custom(format!(
18
+ "client_id {id} exceeds the maximum safe integer (2^53 - 1); \
19
+ Yjs client IDs must be JS-safe integers to avoid collisions"
20
+ ))
21
+ }
22
+
23
+ fn checked_client_id(id: u64) -> Result<ClientID, ReadError> {
24
+ if !is_safe_client_id(id) {
25
+ return Err(unsafe_client_id_error(id));
26
+ }
27
+ Ok(ClientID::new(id))
28
+ }
29
+
30
+ fn validate_raw_client_id(id: u64) -> Result<(), ReadError> {
31
+ if !is_safe_client_id(id) {
32
+ return Err(unsafe_client_id_error(id));
33
+ }
34
+ Ok(())
35
+ }
36
+
37
+ struct CheckedDecoderV1<'a> {
38
+ cursor: Cursor<'a>,
39
+ }
40
+
41
+ impl<'a> CheckedDecoderV1<'a> {
42
+ fn new(cursor: Cursor<'a>) -> Self {
43
+ CheckedDecoderV1 { cursor }
44
+ }
45
+
46
+ fn read_id(&mut self) -> Result<ID, ReadError> {
47
+ let client: u64 = self.read_var()?;
48
+ validate_raw_client_id(client)?;
49
+ let clock = self.read_var()?;
50
+ Ok(ID::new(ClientID::new(client), clock))
51
+ }
52
+ }
53
+
54
+ impl<'a> Read for CheckedDecoderV1<'a> {
55
+ #[inline]
56
+ fn read_u8(&mut self) -> Result<u8, ReadError> {
57
+ self.cursor.read_u8()
58
+ }
59
+
60
+ #[inline]
61
+ fn read_exact(&mut self, len: usize) -> Result<&[u8], ReadError> {
62
+ self.cursor.read_exact(len)
63
+ }
64
+ }
65
+
66
+ impl<'a> Decoder for CheckedDecoderV1<'a> {
67
+ #[inline]
68
+ fn reset_ds_cur_val(&mut self) {}
69
+
70
+ #[inline]
71
+ fn read_ds_clock(&mut self) -> Result<u32, ReadError> {
72
+ self.read_var()
73
+ }
74
+
75
+ #[inline]
76
+ fn read_ds_len(&mut self) -> Result<u32, ReadError> {
77
+ self.read_var()
78
+ }
79
+
80
+ #[inline]
81
+ fn read_left_id(&mut self) -> Result<ID, ReadError> {
82
+ self.read_id()
83
+ }
84
+
85
+ #[inline]
86
+ fn read_right_id(&mut self) -> Result<ID, ReadError> {
87
+ self.read_id()
88
+ }
89
+
90
+ #[inline]
91
+ fn read_client(&mut self) -> Result<ClientID, ReadError> {
92
+ let client: u64 = self.cursor.read_var()?;
93
+ checked_client_id(client)
94
+ }
95
+
96
+ #[inline]
97
+ fn read_info(&mut self) -> Result<u8, ReadError> {
98
+ self.cursor.read_u8()
99
+ }
100
+
101
+ #[inline]
102
+ fn read_parent_info(&mut self) -> Result<bool, ReadError> {
103
+ let info: u32 = self.cursor.read_var()?;
104
+ Ok(info == 1)
105
+ }
106
+
107
+ #[inline]
108
+ fn read_type_ref(&mut self) -> Result<u8, ReadError> {
109
+ self.cursor.read_u8()
110
+ }
111
+
112
+ #[inline]
113
+ fn read_len(&mut self) -> Result<u32, ReadError> {
114
+ self.read_var()
115
+ }
116
+
117
+ #[inline]
118
+ fn read_any(&mut self) -> Result<Any, ReadError> {
119
+ Any::decode(self)
120
+ }
121
+
122
+ #[inline]
123
+ fn read_json(&mut self) -> Result<Any, ReadError> {
124
+ let src = self.read_string()?;
125
+ Any::from_json(src)
126
+ }
127
+
128
+ #[inline]
129
+ fn read_key(&mut self) -> Result<Arc<str>, ReadError> {
130
+ let str: Arc<str> = self.read_string()?.into();
131
+ Ok(str)
132
+ }
133
+
134
+ #[inline]
135
+ fn read_to_end(&mut self) -> Result<&[u8], ReadError> {
136
+ Ok(&self.cursor.buf[self.cursor.next..])
137
+ }
138
+ }
139
+
140
+ pub(crate) fn validate_state_vector_client_ids(bytes: &[u8]) -> Result<(), String> {
141
+ let mut cursor = Cursor::new(bytes);
142
+ let len: u32 = cursor.read_var().map_err(|e| e.to_string())?;
143
+ for _ in 0..len {
144
+ let client: u64 = cursor.read_var().map_err(|e| e.to_string())?;
145
+ validate_raw_client_id(client).map_err(|e| e.to_string())?;
146
+ let _: u32 = cursor.read_var().map_err(|e| e.to_string())?;
147
+ }
148
+ if cursor.has_content() {
149
+ return Err("state vector has trailing bytes".to_string());
150
+ }
151
+ Ok(())
152
+ }
153
+
154
+ pub(crate) fn validate_update_client_ids(update_bytes: &[u8]) -> Result<(), String> {
155
+ let mut decoder = CheckedDecoderV1::new(Cursor::new(update_bytes));
156
+ yrs::Update::decode(&mut decoder).map_err(|e| e.to_string())?;
157
+ if decoder.cursor.has_content() {
158
+ return Err("update has trailing bytes".to_string());
159
+ }
160
+ Ok(())
161
+ }
162
+
163
+ fn validate_awareness_update_client_ids(bytes: &[u8]) -> Result<(), String> {
164
+ let mut cursor = Cursor::new(bytes);
165
+ let len: u32 = cursor.read_var().map_err(|e| e.to_string())?;
166
+ for _ in 0..len {
167
+ let client: u64 = cursor.read_var().map_err(|e| e.to_string())?;
168
+ validate_raw_client_id(client).map_err(|e| e.to_string())?;
169
+ let _: u32 = cursor.read_var().map_err(|e| e.to_string())?;
170
+ let _ = cursor.read_string().map_err(|e| e.to_string())?;
171
+ }
172
+ if cursor.has_content() {
173
+ return Err("awareness update has trailing bytes".to_string());
174
+ }
175
+ Ok(())
176
+ }
177
+
178
+ pub(crate) fn validate_frame_client_ids(bytes: &[u8]) -> Result<(), String> {
179
+ let mut cursor = Cursor::new(bytes);
180
+ while cursor.has_content() {
181
+ let tag: u8 = cursor.read_var().map_err(|e| e.to_string())?;
182
+ match tag {
183
+ MSG_SYNC => {
184
+ let sync_tag: u8 = cursor.read_var().map_err(|e| e.to_string())?;
185
+ let payload = cursor.read_buf().map_err(|e| e.to_string())?;
186
+ match sync_tag {
187
+ MSG_SYNC_STEP_1 => validate_state_vector_client_ids(payload)?,
188
+ MSG_SYNC_STEP_2 | MSG_SYNC_UPDATE => validate_update_client_ids(payload)?,
189
+ _ => return Err("unknown sync message type".to_string()),
190
+ }
191
+ }
192
+ MSG_AWARENESS => {
193
+ let payload = cursor.read_buf().map_err(|e| e.to_string())?;
194
+ validate_awareness_update_client_ids(payload)?;
195
+ }
196
+ MSG_AUTH => {
197
+ let permission: u8 = cursor.read_var().map_err(|e| e.to_string())?;
198
+ if permission == PERMISSION_DENIED {
199
+ let _ = cursor.read_string().map_err(|e| e.to_string())?;
200
+ }
201
+ }
202
+ MSG_QUERY_AWARENESS => {}
203
+ _ => {
204
+ let _ = cursor.read_buf().map_err(|e| e.to_string())?;
205
+ }
206
+ }
207
+ }
208
+ Ok(())
209
+ }
210
+
211
+ /// Classify a frame: a non-zero code only for exactly one well-formed message
212
+ /// that consumes the whole buffer (see `RbAwareness::message_kind` for codes).
213
+ pub(crate) fn classify_message(bytes: &[u8]) -> u8 {
214
+ if validate_frame_client_ids(bytes).is_err() {
215
+ return 0;
216
+ }
217
+ let mut decoder = DecoderV1::new(Cursor::new(bytes));
218
+ let msg = match Message::decode(&mut decoder) {
219
+ Ok(msg) => msg,
220
+ Err(_) => return 0, // empty or malformed
221
+ };
222
+ // Any remaining byte means a second message or trailing garbage.
223
+ if decoder.read_u8().is_ok() {
224
+ return 0;
225
+ }
226
+ match msg {
227
+ Message::Sync(SyncMessage::SyncStep1(_)) => 1,
228
+ Message::Sync(SyncMessage::SyncStep2(_)) | Message::Sync(SyncMessage::Update(_)) => 2,
229
+ Message::Awareness(_) => 3,
230
+ Message::AwarenessQuery => 4,
231
+ _ => 0, // Auth / Custom: not part of our model
232
+ }
233
+ }
234
+
235
+ /// Merge the document-update deltas (Update / SyncStep2 payloads) carried by a
236
+ /// frame into one update, or `None` if the frame carries no document change
237
+ /// (a request, an awareness update, or a no-op handshake SyncStep2).
238
+ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
239
+ validate_frame_client_ids(bytes)?;
240
+ let mut decoder = DecoderV1::new(Cursor::new(bytes));
241
+ let mut updates: Vec<Vec<u8>> = Vec::new();
242
+ for msg in MessageReader::new(&mut decoder) {
243
+ match msg.map_err(|e| e.to_string())? {
244
+ Message::Sync(SyncMessage::Update(u)) | Message::Sync(SyncMessage::SyncStep2(u)) => {
245
+ updates.push(u)
246
+ }
247
+ _ => {}
248
+ }
249
+ }
250
+ let merged = match updates.len() {
251
+ 0 => return Ok(None),
252
+ 1 => updates.pop().unwrap(),
253
+ _ => yrs::merge_updates_v1(&updates).map_err(|e| e.to_string())?,
254
+ };
255
+ let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
256
+ // A genuine no-op (e.g. the empty SyncStep2 in an opening handshake) carries
257
+ // no structs, no deletes, and no dependencies. We must NOT treat a causally-
258
+ // pending update as a no-op: since yrs 0.26 such an update reports an empty
259
+ // state_vector (its structs can't integrate yet), but it still carries
260
+ // content and a non-empty lower bound (the deps it's waiting on). Dropping it
261
+ // here would silently swallow a gappy update instead of rejecting + resyncing.
262
+ if update.state_vector().is_empty()
263
+ && update.delete_set().is_empty()
264
+ && update.state_vector_lower().is_empty()
265
+ {
266
+ return Ok(None);
267
+ }
268
+ Ok(Some(merged))
269
+ }
270
+
271
+ /// Collect the awareness client IDs referenced by a frame's awareness messages.
272
+ pub(crate) fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
273
+ validate_frame_client_ids(bytes)?;
274
+ let mut decoder = DecoderV1::new(Cursor::new(bytes));
275
+ let mut ids = Vec::new();
276
+ for msg in MessageReader::new(&mut decoder) {
277
+ if let Message::Awareness(update) = msg.map_err(|e| e.to_string())? {
278
+ ids.extend(update.clients.keys().map(|c| c.get()));
279
+ }
280
+ }
281
+ Ok(ids)
282
+ }
283
+
284
+ /// True if applying `update_bytes` to `doc` would integrate cleanly: every
285
+ /// dependency the update references is already present (the doc's state vector
286
+ /// covers the update's lower bound). A pure read; does not mutate the doc.
287
+ /// When false, applying it would park a pending struct -- the signal that an
288
+ /// earlier, causally-prior update is missing.
289
+ pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
290
+ validate_update_client_ids(update_bytes)?;
291
+ let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
292
+ Ok(doc.transact().state_vector() >= update.state_vector_lower())
293
+ }
294
+
295
+ /// True if applying `update_bytes` would actually change `doc` -- i.e. it carries
296
+ /// content the doc doesn't already have. Lets the server make durable side
297
+ /// effects exactly-once: a lost-ack retry re-sends an update the server already
298
+ /// applied; that retry is causally ready (so `update_is_ready` is true) but must
299
+ /// NOT re-run `on_change`.
300
+ ///
301
+ /// We can't read the update's own state vector to decide this: yrs reports an
302
+ /// EMPTY state_vector() for a causally-pending diff (e.g. a resync delta whose
303
+ /// structs depend on updates the doc has but the standalone update doesn't),
304
+ /// which would look identical to a no-op. So measure the real effect: seed an
305
+ /// independent probe with the doc's current state, apply the update there, and
306
+ /// see whether the state vector grew. Deletes don't move the state vector, so we
307
+ /// can't cheaply prove a delete-bearing update is a duplicate -- we
308
+ /// conservatively report it as advancing (record it). That can still
309
+ /// double-record a pure-delete retry, but it NEVER drops a real deletion, which
310
+ /// is the safe direction. Assumes the update is already causally ready.
311
+ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
312
+ validate_update_client_ids(update_bytes)?;
313
+ let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
314
+ if !update.delete_set().is_empty() {
315
+ return Ok(true); // can't cheaply prove a delete is a duplicate; record it
316
+ }
317
+ let probe = Doc::new();
318
+ let current = doc
319
+ .transact()
320
+ .encode_state_as_update_v1(&yrs::StateVector::default());
321
+ probe
322
+ .transact_mut()
323
+ .apply_update(yrs::Update::decode_v1(&current).map_err(|e| e.to_string())?)
324
+ .map_err(|e| e.to_string())?;
325
+ let before = probe.transact().state_vector();
326
+ probe
327
+ .transact_mut()
328
+ .apply_update(update)
329
+ .map_err(|e| e.to_string())?;
330
+ let after = probe.transact().state_vector();
331
+ Ok(after != before)
332
+ }
333
+
334
+ /// True if the doc holds pending structs or a pending delete set -- blocks that
335
+ /// couldn't integrate because a dependency is missing. Used as a backstop after
336
+ /// loading from storage: leftover pending means the stored log has a causal gap.
337
+ pub(crate) fn doc_has_pending(doc: &Doc) -> bool {
338
+ let txn = doc.transact();
339
+ txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
340
+ }
341
+
342
+ #[cfg(test)]
343
+ mod tests {
344
+ use super::*;
345
+ use crate::is_safe_client_id;
346
+ use yrs::encoding::write::Write;
347
+ use yrs::sync::Awareness;
348
+ use yrs::updates::encoder::{Encode, Encoder, EncoderV1};
349
+ use yrs::Text;
350
+
351
+ fn text_update(content: &str) -> Vec<u8> {
352
+ let doc = Doc::new();
353
+ let text = doc.get_or_insert_text("content");
354
+ text.insert(&mut doc.transact_mut(), 0, content);
355
+ let update = doc
356
+ .transact()
357
+ .encode_state_as_update_v1(&yrs::StateVector::default());
358
+ update
359
+ }
360
+
361
+ fn update_frame(content: &str) -> Vec<u8> {
362
+ Message::Sync(SyncMessage::Update(text_update(content))).encode_v1()
363
+ }
364
+
365
+ fn step1_frame() -> Vec<u8> {
366
+ Message::Sync(SyncMessage::SyncStep1(yrs::StateVector::default())).encode_v1()
367
+ }
368
+
369
+ fn awareness_frame(client_id: u64) -> Vec<u8> {
370
+ let mut awareness = Awareness::new(Doc::with_client_id(client_id));
371
+ awareness
372
+ .set_local_state(serde_json::json!({ "user": "alice" }))
373
+ .unwrap();
374
+ Message::Awareness(awareness.update().unwrap()).encode_v1()
375
+ }
376
+
377
+ fn unsafe_struct_client_update() -> Vec<u8> {
378
+ let mut update = EncoderV1::new();
379
+ update.write_var(1u32); // client count
380
+ update.write_var(0u32); // block count for this client
381
+ update.write_var(1u64 << 53); // unsafe client id
382
+ update.write_var(0u32); // clock
383
+ update.write_var(0u32); // delete-set client count
384
+ update.to_vec()
385
+ }
386
+
387
+ fn unsafe_awareness_frame() -> Vec<u8> {
388
+ let mut payload = EncoderV1::new();
389
+ payload.write_var(1u32); // client count
390
+ payload.write_var(1u64 << 53); // unsafe client id
391
+ payload.write_var(1u32); // clock
392
+ payload.write_string("{}");
393
+
394
+ let mut frame = EncoderV1::new();
395
+ frame.write_var(MSG_AWARENESS);
396
+ frame.write_buf(payload.to_vec());
397
+ frame.to_vec()
398
+ }
399
+
400
+ fn unsafe_step1_frame() -> Vec<u8> {
401
+ let mut sv = EncoderV1::new();
402
+ sv.write_var(1u32); // state-vector entry count
403
+ sv.write_var(1u64 << 53); // unsafe client id
404
+ sv.write_var(0u32); // clock
405
+
406
+ let mut frame = EncoderV1::new();
407
+ frame.write_var(MSG_SYNC);
408
+ frame.write_var(MSG_SYNC_STEP_1);
409
+ frame.write_buf(sv.to_vec());
410
+ frame.to_vec()
411
+ }
412
+
413
+ #[test]
414
+ fn classify_accepts_clean_single_messages() {
415
+ assert_eq!(classify_message(&step1_frame()), 1);
416
+ assert_eq!(classify_message(&update_frame("hi")), 2);
417
+ assert_eq!(classify_message(&awareness_frame(7)), 3);
418
+ assert_eq!(classify_message(&Message::AwarenessQuery.encode_v1()), 4);
419
+ }
420
+
421
+ #[test]
422
+ fn classify_rejects_unsafe_frames() {
423
+ assert_eq!(classify_message(b""), 0, "empty");
424
+ assert_eq!(classify_message(&[0xff, 0xff, 0xff]), 0, "garbage");
425
+ assert_eq!(classify_message(&[0x63, 0x63, 0x63]), 0, "unknown type");
426
+
427
+ let mut two = update_frame("a");
428
+ two.extend(awareness_frame(1)); // two messages packed together
429
+ assert_eq!(classify_message(&two), 0, "multi-message");
430
+
431
+ let mut trailing = update_frame("a");
432
+ trailing.extend_from_slice(&[0xde, 0xad]);
433
+ assert_eq!(classify_message(&trailing), 0, "trailing garbage");
434
+
435
+ let frame = update_frame("hello");
436
+ assert_eq!(classify_message(&frame[..frame.len() / 2]), 0, "truncated");
437
+ }
438
+
439
+ #[test]
440
+ fn update_advances_is_false_for_an_already_applied_retry() {
441
+ let doc = Doc::new();
442
+ let upd = text_update("hello");
443
+
444
+ // Against a doc that doesn't have it yet, the update advances.
445
+ assert!(
446
+ update_advances_doc(&doc, &upd).unwrap(),
447
+ "new content advances"
448
+ );
449
+
450
+ // Apply it, then the byte-identical retry no longer advances.
451
+ doc.transact_mut()
452
+ .apply_update(yrs::Update::decode_v1(&upd).unwrap())
453
+ .unwrap();
454
+ assert!(
455
+ !update_advances_doc(&doc, &upd).unwrap(),
456
+ "an already-applied retry does not advance"
457
+ );
458
+
459
+ // A genuinely new insert (from a different client) still advances.
460
+ let more = text_update("world");
461
+ assert!(
462
+ update_advances_doc(&doc, &more).unwrap(),
463
+ "different new content advances"
464
+ );
465
+ }
466
+
467
+ #[test]
468
+ fn update_advances_handles_a_dependent_diff_update() {
469
+ // A causally-pending diff (its structs depend on content the doc already
470
+ // has) reports an EMPTY state_vector() in isolation -- a naive check would
471
+ // misread it as a no-op. Verify the trial-apply gets it right.
472
+ let doc = Doc::new();
473
+ let text = doc.get_or_insert_text("content");
474
+ text.insert(&mut doc.transact_mut(), 0, "a");
475
+ let a_update = doc
476
+ .transact()
477
+ .encode_state_as_update_v1(&yrs::StateVector::default());
478
+ let sv_a = doc.transact().state_vector();
479
+ text.insert(&mut doc.transact_mut(), 1, "b");
480
+ let diff = doc.transact().encode_state_as_update_v1(&sv_a); // depends on "a"
481
+
482
+ // A server that has only "a".
483
+ let server = Doc::new();
484
+ server
485
+ .transact_mut()
486
+ .apply_update(yrs::Update::decode_v1(&a_update).unwrap())
487
+ .unwrap();
488
+
489
+ assert!(
490
+ update_advances_doc(&server, &diff).unwrap(),
491
+ "a dependent diff carrying new content advances"
492
+ );
493
+ server
494
+ .transact_mut()
495
+ .apply_update(yrs::Update::decode_v1(&diff).unwrap())
496
+ .unwrap();
497
+ assert!(
498
+ !update_advances_doc(&server, &diff).unwrap(),
499
+ "the byte-identical retry of that diff does not advance"
500
+ );
501
+ }
502
+
503
+ #[test]
504
+ fn client_id_safe_integer_boundary() {
505
+ assert!(is_safe_client_id(0), "zero is fine");
506
+ assert!(
507
+ is_safe_client_id((1 << 53) - 1),
508
+ "2^53 - 1 is the max safe id"
509
+ );
510
+ assert!(!is_safe_client_id(1 << 53), "2^53 is unsafe");
511
+ assert!(!is_safe_client_id(1 << 63), "2^63 is unsafe");
512
+ assert!(!is_safe_client_id(u64::MAX), "u64::MAX is unsafe");
513
+ }
514
+
515
+ #[test]
516
+ fn wire_client_id_validation_rejects_unsafe_sync_update_clients() {
517
+ let update = unsafe_struct_client_update();
518
+ assert!(
519
+ validate_update_client_ids(&update).is_err(),
520
+ "raw unsafe update client id is rejected before yrs can mask it"
521
+ );
522
+
523
+ let frame = Message::Sync(SyncMessage::Update(update)).encode_v1();
524
+ assert_eq!(
525
+ classify_message(&frame),
526
+ 0,
527
+ "unsafe sync frame is not relayable"
528
+ );
529
+ assert!(merged_doc_update(&frame).is_err());
530
+ }
531
+
532
+ #[test]
533
+ fn wire_client_id_validation_rejects_unsafe_awareness_and_step1_clients() {
534
+ assert!(
535
+ validate_frame_client_ids(&unsafe_awareness_frame()).is_err(),
536
+ "raw unsafe awareness client id is rejected"
537
+ );
538
+ assert_eq!(classify_message(&unsafe_awareness_frame()), 0);
539
+ assert!(awareness_client_ids_in(&unsafe_awareness_frame()).is_err());
540
+
541
+ assert!(
542
+ validate_frame_client_ids(&unsafe_step1_frame()).is_err(),
543
+ "raw unsafe state-vector client id is rejected"
544
+ );
545
+ assert_eq!(classify_message(&unsafe_step1_frame()), 0);
546
+ }
547
+
548
+ #[test]
549
+ fn merged_doc_update_extracts_and_skips_no_ops() {
550
+ // A document update yields a delta that reconstructs the content.
551
+ let delta = merged_doc_update(&update_frame("hello"))
552
+ .unwrap()
553
+ .expect("a document update");
554
+ let doc = Doc::new();
555
+ doc.transact_mut()
556
+ .apply_update(yrs::Update::decode_v1(&delta).unwrap())
557
+ .unwrap();
558
+ // The delta carried real content, so applying it advances the doc.
559
+ assert!(!doc.transact().state_vector().is_empty());
560
+
561
+ // A SyncStep1 request carries no document change.
562
+ assert!(merged_doc_update(&step1_frame()).unwrap().is_none());
563
+
564
+ // An empty SyncStep2 (no new structs) is a no-op.
565
+ let empty = Message::Sync(SyncMessage::SyncStep2(
566
+ Doc::new()
567
+ .transact()
568
+ .encode_state_as_update_v1(&yrs::StateVector::default()),
569
+ ))
570
+ .encode_v1();
571
+ assert!(merged_doc_update(&empty).unwrap().is_none());
572
+ }
573
+
574
+ #[test]
575
+ fn merged_doc_update_merges_multiple_updates() {
576
+ // Two updates from different clients packed in one frame merge into one.
577
+ let mut frame = update_frame("a");
578
+ frame.extend(update_frame("b"));
579
+ let merged = merged_doc_update(&frame).unwrap().expect("merged update");
580
+
581
+ // The merged update must decode cleanly as a single update.
582
+ assert!(yrs::Update::decode_v1(&merged).is_ok());
583
+ }
584
+
585
+ #[test]
586
+ fn awareness_client_ids_are_collected() {
587
+ assert_eq!(
588
+ awareness_client_ids_in(&awareness_frame(111)).unwrap(),
589
+ vec![111]
590
+ );
591
+ // A document frame has no awareness client ids.
592
+ assert!(awareness_client_ids_in(&update_frame("x"))
593
+ .unwrap()
594
+ .is_empty());
595
+ }
596
+
597
+ #[test]
598
+ fn update_readiness_and_pending_detect_a_causal_gap() {
599
+ // Three sequential single-char inserts from one client: A, then B, then
600
+ // C. Each delta depends on the previous, so C can't integrate without B.
601
+ let src = Doc::new();
602
+ let txt = src.get_or_insert_text("t");
603
+ let mut deltas: Vec<Vec<u8>> = Vec::new();
604
+ let mut prev = yrs::StateVector::default();
605
+ for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
606
+ txt.insert(&mut src.transact_mut(), i as u32, ch);
607
+ deltas.push(src.transact().encode_state_as_update_v1(&prev));
608
+ prev = src.transact().state_vector();
609
+ }
610
+ let (u1, u2, u3) = (&deltas[0], &deltas[1], &deltas[2]);
611
+
612
+ // A doc holding only u1 (u2 was lost in transit / its record failed):
613
+ let doc = Doc::new();
614
+ doc.transact_mut()
615
+ .apply_update(yrs::Update::decode_v1(u1).unwrap())
616
+ .unwrap();
617
+ assert!(update_is_ready(&doc, u1).unwrap(), "u1 has no missing deps");
618
+ assert!(
619
+ !update_is_ready(&doc, u3).unwrap(),
620
+ "u3 depends on the missing u2"
621
+ );
622
+ assert!(
623
+ !doc_has_pending(&doc),
624
+ "nothing pending until u3 is applied"
625
+ );
626
+
627
+ // Applying u3 anyway parks it as a pending struct.
628
+ doc.transact_mut()
629
+ .apply_update(yrs::Update::decode_v1(u3).unwrap())
630
+ .unwrap();
631
+ assert!(
632
+ doc_has_pending(&doc),
633
+ "u3 is pending: its parent u2 is missing"
634
+ );
635
+
636
+ // Once u2 arrives (via resync), u3 integrates and pending clears.
637
+ doc.transact_mut()
638
+ .apply_update(yrs::Update::decode_v1(u2).unwrap())
639
+ .unwrap();
640
+ assert!(!doc_has_pending(&doc), "u2 arrived; u3 integrated");
641
+ }
642
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YrbLite
4
- VERSION = "0.1.0.beta5"
4
+ VERSION = "0.1.0.beta6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrb-lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.beta5
4
+ version: 0.1.0.beta6
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara
@@ -83,6 +83,7 @@ files:
83
83
  - ext/yrb_lite/Cargo.toml
84
84
  - ext/yrb_lite/extconf.rb
85
85
  - ext/yrb_lite/src/lib.rs
86
+ - ext/yrb_lite/src/protocol.rs
86
87
  - lib/yrb-lite.rb
87
88
  - lib/yrb_lite.rb
88
89
  - lib/yrb_lite/version.rb