yrby 0.8.1 → 0.9.0

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.
data/ext/yrby/src/lib.rs CHANGED
@@ -1,14 +1,21 @@
1
1
  use magnus::{
2
- function, method, prelude::*, Error, ExceptionClass, IntoValue, RArray, RString, Ruby,
2
+ function, method, prelude::*, Error, ExceptionClass, IntoValue, RArray, RHash, RString, Ruby,
3
3
  TryConvert, Value,
4
4
  };
5
- use yrs::sync::{Message, SyncMessage};
5
+ use std::cell::RefCell;
6
+
7
+ use yrs::sync::{Awareness, Message, SyncMessage};
6
8
  use yrs::updates::decoder::Decode;
7
9
  use yrs::updates::encoder::Encode;
8
- use yrs::{Doc, GetString, ReadTxn, Transact};
10
+ use yrs::{DeepObservable, Doc, GetString, ReadTxn, Text, Transact};
9
11
 
12
+ mod array;
13
+ mod map;
10
14
  mod protocol;
11
15
  mod read;
16
+ mod shared;
17
+ mod text;
18
+ mod xml_text;
12
19
  use lexical_yjs_html as lexical_html;
13
20
  use prosemirror_yjs_html as prosemirror_html;
14
21
  use protocol::{
@@ -37,6 +44,10 @@ fn assert_thread_safe() {
37
44
  is_send_sync::<Doc>();
38
45
  is_send_sync::<RbLexical>();
39
46
  is_send_sync::<RbProseMirror>();
47
+ is_send_sync::<map::RbMap>();
48
+ is_send_sync::<array::RbArray>();
49
+ is_send_sync::<text::RbText>();
50
+ is_send_sync::<xml_text::RbXmlText>();
40
51
  }
41
52
 
42
53
  /// Run `f` with the GVL (Global VM Lock) released, so other Ruby threads,
@@ -60,7 +71,17 @@ fn assert_thread_safe() {
60
71
  ///
61
72
  /// Panics inside the closure are caught and re-raised (resumed) after the GVL
62
73
  /// is reacquired, where magnus converts them to Ruby exceptions.
63
- fn nogvl<F, R>(f: F) -> R
74
+ ///
75
+ /// Interrupts (Thread#raise, Thread#kill, Timeout) come back as the error.
76
+ /// `rb_thread_call_without_gvl` would raise one before or after the closure,
77
+ /// and Ruby's raise jumps over Rust frames without running their
78
+ /// destructors. With `RB_NOGVL_INTR_FAIL` Ruby never raises here: it skips
79
+ /// the closure when an interrupt is already pending, and then the closure
80
+ /// runs with the GVL held. After it, `thread_check_ints` makes the check
81
+ /// `rb_thread_call_without_gvl` makes, as a `Result`. That check also
82
+ /// re-arms interrupts still queued behind the first, so a thread that only
83
+ /// calls into yrby still gets a kill queued behind a raise.
84
+ pub(crate) fn nogvl<F, R>(f: F) -> Result<R, Error>
64
85
  where
65
86
  F: FnOnce() -> R + Send,
66
87
  R: Send,
@@ -88,17 +109,23 @@ where
88
109
  result: None,
89
110
  };
90
111
  unsafe {
91
- rb_sys::rb_thread_call_without_gvl(
112
+ rb_sys::rb_nogvl(
92
113
  Some(callback::<F, R>),
93
114
  &mut ctx as *mut Ctx<F, R> as *mut c_void,
94
115
  None,
95
116
  std::ptr::null_mut(),
117
+ rb_sys::RB_NOGVL_INTR_FAIL as _,
96
118
  );
97
119
  }
98
- match ctx.result.expect("nogvl callback did not run") {
120
+ if let Some(func) = ctx.func.take() {
121
+ ctx.result = Some(catch_unwind(AssertUnwindSafe(func)));
122
+ }
123
+ let result = match ctx.result.expect("nogvl callback did not run") {
99
124
  Ok(result) => result,
100
125
  Err(panic) => resume_unwind(panic),
101
- }
126
+ };
127
+ Ruby::get().unwrap().thread_check_ints()?;
128
+ Ok(result)
102
129
  }
103
130
 
104
131
  /// Helper to create a binary Ruby string from bytes. Called only with the GVL
@@ -119,7 +146,7 @@ fn copy_bytes(s: RString) -> Vec<u8> {
119
146
  /// native decode/apply failures surface as a project-specific error rather than
120
147
  /// a generic RuntimeError. Falls back to RuntimeError only if the class somehow
121
148
  /// can't be resolved.
122
- fn yrb_error(msg: String) -> Error {
149
+ pub(crate) fn yrb_error(msg: String) -> Error {
123
150
  let ruby = Ruby::get().unwrap();
124
151
  let class = ruby
125
152
  .eval::<ExceptionClass>("Y::Error")
@@ -133,28 +160,38 @@ fn yrb_error(msg: String) -> Error {
133
160
 
134
161
  impl RbDoc {
135
162
  /// Create a new Doc with an optional client_id
163
+ ///
164
+ /// The document counts text in UTF-16 units, as Yjs does, so positions
165
+ /// agree with every editor's. The Ruby handles convert to characters.
136
166
  fn new(args: &[Value]) -> Result<Self, Error> {
137
- let doc = if args.is_empty() {
138
- Doc::new()
167
+ let mut options = if args.is_empty() {
168
+ yrs::Options::default()
139
169
  } else {
140
170
  let client_id: u64 = TryConvert::try_convert(args[0])?;
141
- Doc::with_client_id(client_id)
171
+ yrs::Options::with_client_id(yrs::block::ClientID::new(client_id))
142
172
  };
143
- Ok(RbDoc(doc))
173
+ options.offset_kind = yrs::OffsetKind::Utf16;
174
+ Ok(RbDoc(Doc::with_options(options)))
175
+ }
176
+
177
+ /// The id this document writes its own edits under. Presence for the same
178
+ /// writer uses it too, so editors tie a caret to its author's edits.
179
+ fn client_id(&self) -> u64 {
180
+ self.0.client_id().get()
144
181
  }
145
182
 
146
- fn encode_state_vector(&self) -> RString {
183
+ fn encode_state_vector(&self) -> Result<RString, Error> {
147
184
  let doc = &self.0;
148
185
  let sv = nogvl(move || {
149
186
  let txn = doc.transact();
150
187
  txn.state_vector().encode_v1()
151
- });
152
- binary_string(&sv)
188
+ })?;
189
+ Ok(binary_string(&sv))
153
190
  }
154
191
 
155
192
  /// Names of the document's root types, so a content reader can find the one
156
193
  /// holding text without knowing it up front.
157
- fn root_names(&self) -> Vec<String> {
194
+ fn root_names(&self) -> Result<Vec<String>, Error> {
158
195
  let doc = &self.0;
159
196
  nogvl(move || {
160
197
  doc.transact()
@@ -164,7 +201,7 @@ impl RbDoc {
164
201
  })
165
202
  }
166
203
 
167
- fn read_text(&self, name: String) -> Option<String> {
204
+ fn read_text(&self, name: String) -> Result<Option<String>, Error> {
168
205
  let doc = &self.0;
169
206
  nogvl(move || {
170
207
  // Exactly ONE transaction per call. Opening a second while the
@@ -178,7 +215,7 @@ impl RbDoc {
178
215
  /// Text of an XML-shaped root, one top-level block per line. The walk +
179
216
  /// block-join logic lives in `read::xml_blocks_text` (pure, Rust-tested);
180
217
  /// this just opens the transaction and resolves the root.
181
- fn read_xml(&self, name: String) -> Option<String> {
218
+ fn read_xml(&self, name: String) -> Result<Option<String>, Error> {
182
219
  let doc = &self.0;
183
220
  nogvl(move || {
184
221
  let txn = doc.transact();
@@ -191,7 +228,7 @@ impl RbDoc {
191
228
  /// recursive). Complements read_text/read_xml for structured shared state.
192
229
  /// Callers parse the JSON (e.g. `JSON.parse(doc.read_map("state"))`). The
193
230
  /// serialization lives in `read::map_json` (pure, Rust-tested).
194
- fn read_map(&self, name: String) -> Option<String> {
231
+ fn read_map(&self, name: String) -> Result<Option<String>, Error> {
195
232
  let doc = &self.0;
196
233
  nogvl(move || {
197
234
  let txn = doc.transact();
@@ -205,7 +242,7 @@ impl RbDoc {
205
242
  /// array: a board's cards, a sheet's rows. Callers parse the JSON (e.g.
206
243
  /// `JSON.parse(doc.read_array("cards"))`). The serialization lives in
207
244
  /// `read::array_json` (pure, Rust-tested).
208
- fn read_array(&self, name: String) -> Option<String> {
245
+ fn read_array(&self, name: String) -> Result<Option<String>, Error> {
209
246
  let doc = &self.0;
210
247
  nogvl(move || {
211
248
  let txn = doc.transact();
@@ -218,7 +255,7 @@ impl RbDoc {
218
255
  /// set: content that couldn't integrate because a causally-prior update is
219
256
  /// missing. Such content is a recovery buffer, not document state; it heals if
220
257
  /// the missing dependency later arrives. A pure read.
221
- fn pending(&self) -> bool {
258
+ fn pending(&self) -> Result<bool, Error> {
222
259
  let doc = &self.0;
223
260
  nogvl(move || has_pending(doc))
224
261
  }
@@ -231,11 +268,162 @@ impl RbDoc {
231
268
  /// stays lossless for raw-update recovery.)
232
269
  fn compacted_state_update(&self) -> Result<RString, Error> {
233
270
  let doc = &self.0;
234
- let update = nogvl(move || integrated_update(doc, &yrs::StateVector::default()))
271
+ let update = nogvl(move || integrated_update(doc, &yrs::StateVector::default()))?
235
272
  .map_err(yrb_error)?;
236
273
  Ok(binary_string(&update))
237
274
  }
238
275
 
276
+ /// A live `Y::Array` handle to the root array named `name` (created if
277
+ /// absent). Writes through it mutate the document and sync to every peer.
278
+ fn get_array(&self, name: String) -> Result<array::RbArray, Error> {
279
+ array::root_array(&self.0, name)
280
+ }
281
+
282
+ /// A live `Y::Text` handle to the root text named `name` (created if
283
+ /// absent). This is what an agent appends into.
284
+ fn get_text(&self, name: String) -> Result<text::RbText, Error> {
285
+ text::root_text(&self.0, name)
286
+ }
287
+
288
+ /// A live `Y::XmlText` handle to the root xml text named `name` (created
289
+ /// if absent). Rich-text editors keep their document here; this is how a
290
+ /// Ruby process writes a paragraph into one.
291
+ fn get_xml_text(&self, name: String) -> Result<xml_text::RbXmlText, Error> {
292
+ xml_text::root_xml_text(&self.0, name)
293
+ }
294
+
295
+ /// A live `Y::Map` handle to the root map named `name` (created if absent).
296
+ /// Unlike `read_map` (a JSON snapshot), the returned handle reads and *writes*
297
+ /// the actual shared map, with the same thread-safety guarantees as the Doc.
298
+ fn get_map(&self, name: String) -> Result<map::RbMap, Error> {
299
+ map::root_map(&self.0, name)
300
+ }
301
+
302
+ /// Apply `update` and report which blocks of the root `XmlText` named
303
+ /// `root` it touched, as ordinals: a change inside a block, a block
304
+ /// added, or a block removed (the ordinal it had). Ordinals count the
305
+ /// root's `XmlText` blocks, as `xml_text(n)` does, so a divider or an
306
+ /// image above a block doesn't shift it. This is what lets a process
307
+ /// following a document react to the part that changed.
308
+ fn apply_update_changes(&self, update: RString, root: String) -> Result<RArray, Error> {
309
+ let bytes = copy_bytes(update);
310
+ let doc = &self.0;
311
+ let changed = nogvl(move || -> Result<Vec<u32>, String> {
312
+ // One transaction from the snapshot of the blocks to the commit,
313
+ // under an origin of its own: another write can't change the
314
+ // blocks in between, and its events don't count.
315
+ let key = shared::observer_key();
316
+ let mut txn = doc.transact_mut_with(key.clone());
317
+ let fragment = yrs::WriteTxn::get_or_insert_xml_fragment(&mut txn, root.as_str());
318
+ let branch: &yrs::branch::Branch = fragment.as_ref();
319
+ let target = yrs::XmlTextRef::from(yrs::branch::BranchPtr::from(branch));
320
+ let hits = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u32>::new()));
321
+ let sink = hits.clone();
322
+ let root_ref = target.clone();
323
+ let before = Children::of(&txn, &target);
324
+ let ours = key.clone();
325
+ target.observe_deep(key.clone(), move |txn, events| {
326
+ if txn.origin() != Some(&ours) {
327
+ return;
328
+ }
329
+ let children = Children::of(txn, &root_ref);
330
+ let mut hits = sink.lock().unwrap();
331
+ for event in events.iter() {
332
+ match event.path().front() {
333
+ Some(&yrs::types::PathSegment::Index(i)) if children.is_block(i) => {
334
+ hits.push(children.ordinal(i))
335
+ }
336
+ Some(_) => {}
337
+ None => root_positions(txn, event, &before, &children, &mut hits),
338
+ }
339
+ }
340
+ });
341
+ let applied = yrs::Update::decode_v1(&bytes)
342
+ .map_err(|e| e.to_string())
343
+ .and_then(|parsed| txn.apply_update(parsed).map_err(|e| e.to_string()));
344
+ // Committing runs the observer.
345
+ drop(txn);
346
+ target.unobserve_deep(key);
347
+ applied?;
348
+ let mut out = hits.lock().unwrap().clone();
349
+ out.sort_unstable();
350
+ out.dedup();
351
+ Ok(out)
352
+ })?
353
+ .map_err(yrb_error)?;
354
+ let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
355
+ let array = ruby.ary_new();
356
+ for i in changed {
357
+ array.push(i)?;
358
+ }
359
+ Ok(array)
360
+ }
361
+
362
+ /// The ordinal of the top-level block of the root `XmlText` named `root`
363
+ /// that a relative position (an anchor's, or the `{type, tname, item,
364
+ /// assoc}` hash a peer carries as a caret) falls in, or nil. This is how
365
+ /// a process knows which block a person is writing in.
366
+ fn block_at(&self, position: RHash, root: String) -> Result<Option<u32>, Error> {
367
+ let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
368
+ let sticky = sticky_from_ruby(&ruby, position)?;
369
+ let doc = &self.0;
370
+ nogvl(move || {
371
+ let txn = doc.transact();
372
+ let target = sticky.get_offset(&txn)?.branch;
373
+ let root_ref = shared::root_xml_text(&txn, root.as_str())?;
374
+ shared::embedded_xml_texts(&txn, &root_ref)
375
+ .iter()
376
+ .position(|block| contains_branch(&txn, block, target))
377
+ .map(|ordinal| ordinal as u32)
378
+ })
379
+ }
380
+
381
+ /// The block an anchor's position names, as a live handle, or nil once
382
+ /// the block is gone.
383
+ fn find(&self, position: RHash, root: String) -> Result<Option<xml_text::RbXmlText>, Error> {
384
+ let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
385
+ let sticky = sticky_from_ruby(&ruby, position)?;
386
+ let (doc, name) = (&self.0, root.as_str());
387
+ let id = nogvl(move || {
388
+ let txn = doc.transact();
389
+ let branch = sticky.get_offset(&txn)?.branch;
390
+ let id = shared::identity(&yrs::XmlTextRef::from(branch));
391
+ let is_block = matches!(id, yrs::branch::BranchID::Root(_))
392
+ || matches!(branch.type_ref(), yrs::types::TypeRef::XmlText);
393
+ let in_root = shared::identity(&yrs::XmlTextRef::from(shared::top(branch)))
394
+ == yrs::branch::BranchID::Root(name.into());
395
+ (is_block && in_root && shared::resolve::<yrs::XmlTextRef, _>(&txn, &id).is_some())
396
+ .then_some(id)
397
+ })?;
398
+ Ok(id.map(|id| xml_text::handle(&self.0, id, root)))
399
+ }
400
+
401
+ /// The document's state vector and delete set, encoded: where `Doc#edit`
402
+ /// starts.
403
+ fn snapshot(&self) -> Result<RString, Error> {
404
+ let doc = &self.0;
405
+ let snapshot = nogvl(move || doc.transact().snapshot().encode_v1())?;
406
+ Ok(binary_string(&snapshot))
407
+ }
408
+
409
+ /// The update holding what the document gained since `snapshot`, or nil
410
+ /// when it is unchanged: what `Doc#edit` returns.
411
+ fn update_since(&self, snapshot: RString) -> Result<Option<RString>, Error> {
412
+ let bytes = copy_bytes(snapshot);
413
+ let doc = &self.0;
414
+ let update = nogvl(move || -> Result<Option<Vec<u8>>, String> {
415
+ let before = yrs::Snapshot::decode_v1(&bytes).map_err(|e| e.to_string())?;
416
+ let txn = doc.transact();
417
+ let now = txn.snapshot();
418
+ if now == before {
419
+ return Ok(None);
420
+ }
421
+ Ok(Some(changes_since(&txn, &before, &now)))
422
+ })?
423
+ .map_err(yrb_error)?;
424
+ Ok(update.map(|update| binary_string(&update)))
425
+ }
426
+
239
427
  /// Encode state as update (optionally diffed against a state vector)
240
428
  fn encode_state_as_update(&self, args: &[Value]) -> Result<RString, Error> {
241
429
  let sv_bytes: Option<Vec<u8>> = if args.is_empty() {
@@ -252,7 +440,7 @@ impl RbDoc {
252
440
  };
253
441
  let txn = doc.transact();
254
442
  Ok(txn.encode_state_as_update_v1(&sv))
255
- })
443
+ })?
256
444
  .map_err(yrb_error)?;
257
445
  Ok(binary_string(&update))
258
446
  }
@@ -264,7 +452,7 @@ impl RbDoc {
264
452
  let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
265
453
  let mut txn = doc.transact_mut();
266
454
  txn.apply_update(update).map_err(|e| e.to_string())
267
- })
455
+ })?
268
456
  .map_err(yrb_error)
269
457
  }
270
458
 
@@ -274,7 +462,7 @@ impl RbDoc {
274
462
  fn update_ready(&self, update: RString) -> Result<bool, Error> {
275
463
  let update_bytes = copy_bytes(update);
276
464
  let doc = &self.0;
277
- nogvl(move || update_is_ready(doc, &update_bytes)).map_err(yrb_error)
465
+ nogvl(move || update_is_ready(doc, &update_bytes))?.map_err(yrb_error)
278
466
  }
279
467
 
280
468
  /// True if applying `update` would change the document (it carries new
@@ -283,18 +471,18 @@ impl RbDoc {
283
471
  fn update_advances(&self, update: RString) -> Result<bool, Error> {
284
472
  let update_bytes = copy_bytes(update);
285
473
  let doc = &self.0;
286
- nogvl(move || update_advances_doc(doc, &update_bytes)).map_err(yrb_error)
474
+ nogvl(move || update_advances_doc(doc, &update_bytes))?.map_err(yrb_error)
287
475
  }
288
476
 
289
477
  /// Sync step 1: Create a sync message with our state vector
290
- fn sync_step1(&self) -> RString {
478
+ fn sync_step1(&self) -> Result<RString, Error> {
291
479
  let doc = &self.0;
292
480
  let encoded = nogvl(move || {
293
481
  let txn = doc.transact();
294
482
  let sv = txn.state_vector();
295
483
  Message::Sync(SyncMessage::SyncStep1(sv)).encode_v1()
296
- });
297
- binary_string(&encoded)
484
+ })?;
485
+ Ok(binary_string(&encoded))
298
486
  }
299
487
 
300
488
  /// Handle a Sync or Awareness message, returning
@@ -343,7 +531,7 @@ impl RbDoc {
343
531
  // protocol; reject rather than pretend to handle them.
344
532
  _ => Err("unsupported message type".to_string()),
345
533
  }
346
- })
534
+ })?
347
535
  .map_err(yrb_error)?;
348
536
 
349
537
  Ok((msg_type, sync_type, binary_string(&response)))
@@ -396,7 +584,7 @@ impl RbLexical {
396
584
  let txn = doc.transact();
397
585
  let fragment = txn.get_xml_fragment(name.as_str())?;
398
586
  lexical_html::render_segments(&txn, &fragment, rules)
399
- });
587
+ })?;
400
588
  segments_result(segments)
401
589
  }
402
590
 
@@ -410,7 +598,7 @@ impl RbLexical {
410
598
  let txn = doc.transact();
411
599
  let fragment = txn.get_xml_fragment(name.as_str())?;
412
600
  lexical_html::collect_node_types(&txn, &fragment)
413
- });
601
+ })?;
414
602
  let ruby = Ruby::get().unwrap();
415
603
  match map {
416
604
  None => Ok(ruby.qnil().as_value()),
@@ -476,7 +664,7 @@ impl RbProseMirror {
476
664
  let txn = doc.transact();
477
665
  let fragment = txn.get_xml_fragment(name.as_str())?;
478
666
  prosemirror_html::render_segments(&txn, &fragment, rules)
479
- });
667
+ })?;
480
668
  segments_result(segments)
481
669
  }
482
670
 
@@ -490,7 +678,7 @@ impl RbProseMirror {
490
678
  let txn = doc.transact();
491
679
  let fragment = txn.get_xml_fragment(name.as_str())?;
492
680
  prosemirror_html::collect_node_types(&txn, &fragment)
493
- });
681
+ })?;
494
682
  let ruby = Ruby::get().unwrap();
495
683
  match map {
496
684
  None => Ok(ruby.qnil().as_value()),
@@ -598,7 +786,7 @@ fn wrap_update(update: RString) -> RString {
598
786
  /// 2 = sync step2/update (a document change: record/apply/relay)
599
787
  /// 3 = awareness (presence: relay)
600
788
  /// 4 = awareness query (a request: respond, do not relay)
601
- fn message_kind(data: RString) -> u8 {
789
+ fn message_kind(data: RString) -> Result<u8, Error> {
602
790
  let data_bytes = copy_bytes(data);
603
791
  nogvl(move || classify_message(&data_bytes))
604
792
  }
@@ -609,7 +797,7 @@ fn message_kind(data: RString) -> u8 {
609
797
  /// awareness update). The store-backed path records this exact delta before relay.
610
798
  fn update_from_message(data: RString) -> Result<Option<RString>, Error> {
611
799
  let data_bytes = copy_bytes(data);
612
- let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(yrb_error)?;
800
+ let merged = nogvl(move || merged_doc_update(&data_bytes))?.map_err(yrb_error)?;
613
801
  Ok(merged.map(|b| binary_string(&b)))
614
802
  }
615
803
 
@@ -617,6 +805,356 @@ fn update_from_message(data: RString) -> Result<Option<RString>, Error> {
617
805
  // Module Initialization
618
806
  // ============================================================================
619
807
 
808
+ /// The blocks added since `before`, and the deletions made since it.
809
+ ///
810
+ /// An update since a state vector carries the whole delete set, every
811
+ /// deletion the document has ever had, so a chunk streamed into a long-lived
812
+ /// document would grow with its history. A v1 update is its blocks followed
813
+ /// by its delete set, so this swaps that delete set for only the new
814
+ /// deletions. If the bytes ever don't end with the delete set as expected, it
815
+ /// keeps the whole update, which is bigger but still right.
816
+ fn changes_since<T: ReadTxn>(txn: &T, before: &yrs::Snapshot, now: &yrs::Snapshot) -> Vec<u8> {
817
+ let full = txn.encode_state_as_update_v1(&before.state_map);
818
+ let all = now.delete_set.encode_v1();
819
+ match full.strip_suffix(all.as_slice()) {
820
+ Some(blocks) => {
821
+ let mut update = blocks.to_vec();
822
+ update.extend(now.delete_set.diff(&before.delete_set).encode_v1());
823
+ update
824
+ }
825
+ None => full,
826
+ }
827
+ }
828
+
829
+ /// A root's children in order: the document index each starts at and
830
+ /// whether it is an `XmlText` block. Ordinals count only those, as
831
+ /// `xml_text(n)` does; a divider or an image is a child but not a block.
832
+ struct Children(Vec<(u32, bool)>);
833
+
834
+ impl Children {
835
+ fn of<T: ReadTxn>(txn: &T, root: &yrs::XmlTextRef) -> Self {
836
+ let mut at = 0;
837
+ let mut children = Vec::new();
838
+ for d in root.diff(txn, yrs::types::text::YChange::identity) {
839
+ let (len, block) = match &d.insert {
840
+ yrs::Out::Any(yrs::Any::String(s)) => (s.encode_utf16().count() as u32, false),
841
+ yrs::Out::YXmlText(_) => (1, true),
842
+ _ => (1, false),
843
+ };
844
+ children.push((at, block));
845
+ at += len;
846
+ }
847
+ Children(children)
848
+ }
849
+
850
+ /// How many blocks come before document index `index`: the ordinal of a
851
+ /// block there.
852
+ fn ordinal(&self, index: u32) -> u32 {
853
+ self.0
854
+ .iter()
855
+ .filter(|(at, block)| *block && *at < index)
856
+ .count() as u32
857
+ }
858
+
859
+ fn is_block(&self, index: u32) -> bool {
860
+ self.0.iter().any(|(at, block)| *block && *at == index)
861
+ }
862
+
863
+ /// How many blocks start in `from..to`.
864
+ fn blocks_in(&self, from: u32, to: u32) -> u32 {
865
+ self.0
866
+ .iter()
867
+ .filter(|(at, block)| *block && (from..to).contains(at))
868
+ .count() as u32
869
+ }
870
+ }
871
+
872
+ /// A presence handle a Ruby process can use to appear as a live collaborator.
873
+ ///
874
+ /// Wraps a yrs `Awareness`. `set_local_state` takes a JSON string (the shape the
875
+ /// editor renders, e.g. `{"name":"Agent","color":"#7c3aed"}`) and returns the
876
+ /// y-protocol awareness frame to broadcast. Browsers subscribed to the document
877
+ /// apply it as another participant. `clear_local_state` returns the frame that
878
+ /// removes this presence. The bytes are the same wire format Yjs and
879
+ /// y-protocols use, so no client change is needed.
880
+ #[magnus::wrap(class = "Y::Awareness", free_immediately, size)]
881
+ struct RbAwareness(RefCell<Awareness>);
882
+
883
+ impl RbAwareness {
884
+ fn new(args: &[Value]) -> Result<Self, Error> {
885
+ let doc = if args.is_empty() || args[0].is_nil() {
886
+ Doc::new()
887
+ } else {
888
+ let client_id: u64 = TryConvert::try_convert(args[0])?;
889
+ Doc::with_client_id(client_id)
890
+ };
891
+ Ok(RbAwareness(RefCell::new(Awareness::new(doc))))
892
+ }
893
+
894
+ /// This presence's client id, stable for the life of the handle.
895
+ fn client_id(&self) -> u64 {
896
+ self.0.borrow().client_id().get()
897
+ }
898
+
899
+ /// Set this client's presence from a JSON string and return the awareness
900
+ /// frame to broadcast.
901
+ fn set_local_state(&self, json: String) -> Result<RString, Error> {
902
+ let value: serde_json::Value = serde_json::from_str(&json)
903
+ .map_err(|e| yrb_error(format!("Y::Awareness state must be JSON: {e}")))?;
904
+ let mut awareness = self.0.borrow_mut();
905
+ awareness
906
+ .set_local_state(value)
907
+ .map_err(|e| yrb_error(e.to_string()))?;
908
+ Self::frame(&awareness)
909
+ }
910
+
911
+ /// Remove this client's presence and return the frame that tells peers to
912
+ /// drop it: an entry with the next clock and a null state, as y-protocols
913
+ /// encodes a removal. yrs's own clean just forgets the client, and a
914
+ /// frame built from that says nothing, so peers would wait for a timeout.
915
+ fn clear_local_state(&self) -> Result<RString, Error> {
916
+ let mut awareness = self.0.borrow_mut();
917
+ let id = awareness.client_id();
918
+ let clock = awareness
919
+ .iter()
920
+ .find(|(client, _)| *client == id)
921
+ .map(|(_, state)| state.clock)
922
+ .unwrap_or(0);
923
+ awareness.clean_local_state();
924
+ let mut clients = std::collections::HashMap::new();
925
+ clients.insert(
926
+ id,
927
+ yrs::sync::awareness::AwarenessUpdateEntry {
928
+ clock: clock + 1,
929
+ json: "null".into(),
930
+ },
931
+ );
932
+ let update = yrs::sync::awareness::AwarenessUpdate { clients };
933
+ Ok(binary_string(&Message::Awareness(update).encode_v1()))
934
+ }
935
+
936
+ /// Apply a presence frame from another client (the bytes a browser or
937
+ /// another process broadcast). Returns false for a frame that is not an
938
+ /// awareness message.
939
+ fn apply_update(&self, frame: RString) -> Result<bool, Error> {
940
+ let bytes = copy_bytes(frame);
941
+ let message = Message::decode_v1(&bytes).map_err(|e| yrb_error(e.to_string()))?;
942
+ match message {
943
+ Message::Awareness(update) => {
944
+ self.0
945
+ .borrow_mut()
946
+ .apply_update(update)
947
+ .map_err(|e| yrb_error(e.to_string()))?;
948
+ Ok(true)
949
+ }
950
+ _ => Ok(false),
951
+ }
952
+ }
953
+
954
+ /// Every client's state, as `{ client_id => state }`, the state being the
955
+ /// JSON each client set (parsed), or nil for a client that cleared it.
956
+ /// Each client's awareness clock. A client renews its state every few
957
+ /// seconds with a higher clock, so a clock that stops moving means the
958
+ /// client is gone without having said so. A mirror uses this to time
959
+ /// peers out the way browser clients do.
960
+ fn clocks(&self) -> Result<RHash, Error> {
961
+ let awareness = self.0.borrow();
962
+ let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
963
+ let out = ruby.hash_new();
964
+ for (client, state) in awareness.iter() {
965
+ out.aset(client.get(), state.clock)?;
966
+ }
967
+ Ok(out)
968
+ }
969
+
970
+ fn states(&self) -> Result<RHash, Error> {
971
+ let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
972
+ let entries: Vec<(u64, Option<String>)> = self
973
+ .0
974
+ .borrow()
975
+ .iter()
976
+ .map(|(id, state)| (id.get(), state.data.as_ref().map(|d| d.to_string())))
977
+ .collect();
978
+ let out = ruby.hash_new();
979
+ for (id, data) in entries {
980
+ let value: Value = match data {
981
+ Some(json) => {
982
+ let parsed: serde_json::Value = serde_json::from_str(&json)
983
+ .map_err(|e| yrb_error(format!("presence state is not JSON: {e}")))?;
984
+ json_to_ruby(&ruby, &parsed)
985
+ }
986
+ None => ruby.qnil().as_value(),
987
+ };
988
+ out.aset(id, value)?;
989
+ }
990
+ Ok(out)
991
+ }
992
+
993
+ fn frame(awareness: &Awareness) -> Result<RString, Error> {
994
+ let update = awareness.update().map_err(|e| yrb_error(e.to_string()))?;
995
+ Ok(binary_string(&Message::Awareness(update).encode_v1()))
996
+ }
997
+ }
998
+
999
+ /// For an event on the root sequence itself (blocks added or removed), the
1000
+ /// ordinals affected. A delta walks the root as it was (`before`): retained
1001
+ /// and removed children advance through it, inserted ones through the root
1002
+ /// as it is now (`after`). An added block reports its new ordinal, a removed
1003
+ /// one the ordinal it had, and a divider or image reports nothing.
1004
+ fn root_positions(
1005
+ txn: &yrs::TransactionMut,
1006
+ event: &yrs::types::Event,
1007
+ before: &Children,
1008
+ after: &Children,
1009
+ hits: &mut Vec<u32>,
1010
+ ) {
1011
+ use yrs::types::{Change, Delta, Event};
1012
+ let (mut old, mut new) = (0u32, 0u32);
1013
+ let added = |item: &yrs::Out, new: &mut u32, hits: &mut Vec<u32>| {
1014
+ if matches!(item, yrs::Out::YXmlText(_)) {
1015
+ hits.push(after.ordinal(*new));
1016
+ }
1017
+ *new += match item {
1018
+ yrs::Out::Any(yrs::Any::String(s)) => s.encode_utf16().count() as u32,
1019
+ _ => 1,
1020
+ };
1021
+ };
1022
+ let removed = |old: u32, n: u32, hits: &mut Vec<u32>| {
1023
+ let first = before.ordinal(old);
1024
+ hits.extend(first..first + before.blocks_in(old, old + n));
1025
+ };
1026
+ match event {
1027
+ Event::XmlFragment(e) => {
1028
+ for change in e.delta(txn) {
1029
+ match change {
1030
+ Change::Retain(n) => (old, new) = (old + n, new + n),
1031
+ Change::Added(items) => {
1032
+ for item in items {
1033
+ added(item, &mut new, hits);
1034
+ }
1035
+ }
1036
+ Change::Removed(n) => {
1037
+ removed(old, *n, hits);
1038
+ old += n;
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ Event::XmlText(e) => {
1044
+ for delta in e.delta(txn) {
1045
+ match delta {
1046
+ Delta::Retain(n, _) => (old, new) = (old + n, new + n),
1047
+ Delta::Inserted(item, _) => added(item, &mut new, hits),
1048
+ Delta::Deleted(n) => {
1049
+ removed(old, *n, hits);
1050
+ old += n;
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+ _ => {}
1056
+ }
1057
+ }
1058
+
1059
+ /// Whether `branch` is `block` itself or embedded anywhere inside it.
1060
+ fn contains_branch<T: ReadTxn>(
1061
+ txn: &T,
1062
+ block: &yrs::XmlTextRef,
1063
+ branch: yrs::branch::BranchPtr,
1064
+ ) -> bool {
1065
+ let this: &yrs::branch::Branch = block.as_ref();
1066
+ if yrs::branch::BranchPtr::from(this) == branch {
1067
+ return true;
1068
+ }
1069
+ for d in block.diff(txn, yrs::types::text::YChange::identity) {
1070
+ if let yrs::Out::YXmlText(child) = &d.insert {
1071
+ if contains_branch(txn, child, branch) {
1072
+ return true;
1073
+ }
1074
+ }
1075
+ }
1076
+ false
1077
+ }
1078
+
1079
+ fn hash_get(ruby: &Ruby, h: RHash, key: &str) -> Option<Value> {
1080
+ h.get(ruby.to_symbol(key))
1081
+ .or_else(|| h.get(key))
1082
+ .filter(|v| !v.is_nil())
1083
+ }
1084
+
1085
+ /// A yrs sticky index from the `{type, tname, item, assoc}` hash Yjs uses.
1086
+ fn sticky_from_ruby(ruby: &Ruby, position: RHash) -> Result<yrs::StickyIndex, Error> {
1087
+ use yrs::{Assoc, IndexScope, StickyIndex};
1088
+ let assoc = match hash_get(ruby, position, "assoc") {
1089
+ Some(v) => {
1090
+ let n: i64 = TryConvert::try_convert(v)?;
1091
+ if n < 0 {
1092
+ Assoc::Before
1093
+ } else {
1094
+ Assoc::After
1095
+ }
1096
+ }
1097
+ None => Assoc::After,
1098
+ };
1099
+ let id_of = |key: &str| -> Result<Option<yrs::block::ID>, Error> {
1100
+ let Some(v) = hash_get(ruby, position, key) else {
1101
+ return Ok(None);
1102
+ };
1103
+ let h = RHash::from_value(v).ok_or_else(|| yrb_error(format!("{key} must be a hash")))?;
1104
+ let client: u64 = TryConvert::try_convert(
1105
+ hash_get(ruby, h, "client").ok_or_else(|| yrb_error("missing client".into()))?,
1106
+ )?;
1107
+ let clock: u32 = TryConvert::try_convert(
1108
+ hash_get(ruby, h, "clock").ok_or_else(|| yrb_error("missing clock".into()))?,
1109
+ )?;
1110
+ Ok(Some(yrs::block::ID::new(
1111
+ yrs::block::ClientID::new(client),
1112
+ clock,
1113
+ )))
1114
+ };
1115
+ if let Some(item) = id_of("item")? {
1116
+ return Ok(StickyIndex::from_id(item, assoc));
1117
+ }
1118
+ if let Some(ty) = id_of("type")? {
1119
+ return Ok(StickyIndex::new(IndexScope::Nested(ty), assoc));
1120
+ }
1121
+ match hash_get(ruby, position, "tname") {
1122
+ Some(v) => {
1123
+ let name: String = TryConvert::try_convert(v)?;
1124
+ Ok(StickyIndex::new(IndexScope::Root(name.into()), assoc))
1125
+ }
1126
+ None => Err(yrb_error("a position needs item, type, or tname".into())),
1127
+ }
1128
+ }
1129
+
1130
+ /// serde_json to Ruby, for presence states.
1131
+ fn json_to_ruby(ruby: &Ruby, v: &serde_json::Value) -> Value {
1132
+ match v {
1133
+ serde_json::Value::Null => ruby.qnil().as_value(),
1134
+ serde_json::Value::Bool(b) => b.into_value_with(ruby),
1135
+ serde_json::Value::Number(n) => match (n.as_i64(), n.as_u64()) {
1136
+ (Some(i), _) => i.into_value_with(ruby),
1137
+ (None, Some(u)) => u.into_value_with(ruby),
1138
+ _ => n.as_f64().unwrap_or(0.0).into_value_with(ruby),
1139
+ },
1140
+ serde_json::Value::String(s) => s.as_str().into_value_with(ruby),
1141
+ serde_json::Value::Array(a) => {
1142
+ let arr = ruby.ary_new();
1143
+ for x in a {
1144
+ let _ = arr.push(json_to_ruby(ruby, x));
1145
+ }
1146
+ arr.as_value()
1147
+ }
1148
+ serde_json::Value::Object(o) => {
1149
+ let h = ruby.hash_new();
1150
+ for (k, x) in o {
1151
+ let _ = h.aset(k.as_str(), json_to_ruby(ruby, x));
1152
+ }
1153
+ h.as_value()
1154
+ }
1155
+ }
1156
+ }
1157
+
620
1158
  #[magnus::init]
621
1159
  fn init(ruby: &Ruby) -> Result<(), Error> {
622
1160
  let module = ruby.define_module("Y")?;
@@ -637,6 +1175,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
637
1175
  method!(RbDoc::encode_state_as_update, -1),
638
1176
  )?;
639
1177
  doc_class.define_method("apply_update", method!(RbDoc::apply_update, 1))?;
1178
+ doc_class.define_method("client_id", method!(RbDoc::client_id, 0))?;
1179
+ doc_class.define_method(
1180
+ "apply_update_changes",
1181
+ method!(RbDoc::apply_update_changes, 2),
1182
+ )?;
1183
+ doc_class.define_private_method("native_block_at", method!(RbDoc::block_at, 2))?;
1184
+ doc_class.define_private_method("native_find", method!(RbDoc::find, 2))?;
1185
+ doc_class.define_private_method("native_snapshot", method!(RbDoc::snapshot, 0))?;
1186
+ doc_class.define_private_method("native_update_since", method!(RbDoc::update_since, 1))?;
640
1187
  doc_class.define_method("root_names", method!(RbDoc::root_names, 0))?;
641
1188
  doc_class.define_method("read_text", method!(RbDoc::read_text, 1))?;
642
1189
  doc_class.define_method("read_xml", method!(RbDoc::read_xml, 1))?;
@@ -647,6 +1194,10 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
647
1194
  "compacted_state_update",
648
1195
  method!(RbDoc::compacted_state_update, 0),
649
1196
  )?;
1197
+ doc_class.define_method("get_map", method!(RbDoc::get_map, 1))?;
1198
+ doc_class.define_method("get_array", method!(RbDoc::get_array, 1))?;
1199
+ doc_class.define_method("get_text", method!(RbDoc::get_text, 1))?;
1200
+ doc_class.define_method("get_xml_text", method!(RbDoc::get_xml_text, 1))?;
650
1201
  doc_class.define_method("update_ready?", method!(RbDoc::update_ready, 1))?;
651
1202
  doc_class.define_method("update_advances?", method!(RbDoc::update_advances, 1))?;
652
1203
  doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
@@ -666,6 +1217,24 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
666
1217
  prosemirror_class.define_method("to_html", method!(RbProseMirror::native_to_html, -1))?;
667
1218
  prosemirror_class.define_method("node_types", method!(RbProseMirror::node_types, -1))?;
668
1219
 
1220
+ let awareness_class = module.define_class("Awareness", ruby.class_object())?;
1221
+ awareness_class.define_singleton_method("new", function!(RbAwareness::new, -1))?;
1222
+ awareness_class.define_method("client_id", method!(RbAwareness::client_id, 0))?;
1223
+ awareness_class.define_method("set_local_state", method!(RbAwareness::set_local_state, 1))?;
1224
+ awareness_class.define_method(
1225
+ "clear_local_state",
1226
+ method!(RbAwareness::clear_local_state, 0),
1227
+ )?;
1228
+ awareness_class.define_method("apply_update", method!(RbAwareness::apply_update, 1))?;
1229
+ awareness_class.define_method("states", method!(RbAwareness::states, 0))?;
1230
+ awareness_class.define_method("clocks", method!(RbAwareness::clocks, 0))?;
1231
+
1232
+ // Live shared-type handles.
1233
+ map::define(ruby, module)?;
1234
+ array::define(ruby, module)?;
1235
+ text::define(ruby, module)?;
1236
+ xml_text::define(ruby, module)?;
1237
+
669
1238
  // Stateless protocol codec, as Y module functions.
670
1239
  module.define_module_function("wrap_update", function!(wrap_update, 1))?;
671
1240
  module.define_module_function("message_kind", function!(message_kind, 1))?;
@@ -680,3 +1249,343 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
680
1249
 
681
1250
  Ok(())
682
1251
  }
1252
+
1253
+ #[cfg(test)]
1254
+ mod tests {
1255
+ //! Each method on `Y::Doc`, `Y::Doc::Recording`, `Y::Awareness`, and the
1256
+ //! two renderers, called as Ruby calls it. The handles' methods belong to
1257
+ //! their own modules, so these tests write through yrs, under the
1258
+ //! handles' origin where that matters.
1259
+ use super::*;
1260
+ use crate::shared::test_support::rb;
1261
+ use rb_sys_test_helpers::ruby_test;
1262
+ use yrs::{Any, MapPrelim, Text, Xml, XmlTextPrelim, XmlTextRef};
1263
+
1264
+ fn new_doc() -> RbDoc {
1265
+ RbDoc::new(&[]).unwrap()
1266
+ }
1267
+
1268
+ /// Append to the root text `name` as a handle does: under its origin.
1269
+ fn push(doc: &RbDoc, name: &str, chunk: &str) {
1270
+ let text = doc.0.get_or_insert_text(name);
1271
+ let mut txn = doc.0.transact_mut_with(shared::ORIGIN);
1272
+ let at = text.len(&txn);
1273
+ text.insert(&mut txn, at, chunk);
1274
+ }
1275
+
1276
+ fn update(doc: &RbDoc) -> RString {
1277
+ doc.encode_state_as_update(&[]).unwrap()
1278
+ }
1279
+
1280
+ fn update_since(doc: &RbDoc, state_vector: RString) -> RString {
1281
+ doc.encode_state_as_update(&[state_vector.as_value()])
1282
+ .unwrap()
1283
+ }
1284
+
1285
+ fn root(doc: &RbDoc) -> XmlTextRef {
1286
+ doc.0.get_or_insert_xml_fragment("root");
1287
+ shared::root_xml_text(&doc.0.transact(), "root").unwrap()
1288
+ }
1289
+
1290
+ /// Add a Lexical paragraph to `root` before its `at`-th child.
1291
+ fn paragraph(doc: &RbDoc, at: u32, words: &str) {
1292
+ let root = root(doc);
1293
+ let mut txn = doc.0.transact_mut();
1294
+ let block = root.insert_embed(&mut txn, at, XmlTextPrelim::new(""));
1295
+ block.insert_attribute(&mut txn, "__type", "paragraph");
1296
+ let node = MapPrelim::from_iter([
1297
+ ("__type", Any::from("text")),
1298
+ ("__format", Any::from(0)),
1299
+ ("__style", Any::from("")),
1300
+ ("__mode", Any::from(0)),
1301
+ ("__detail", Any::from(0)),
1302
+ ]);
1303
+ block.insert_embed(&mut txn, 0, node);
1304
+ block.insert(&mut txn, 1, words);
1305
+ }
1306
+
1307
+ fn lexical_doc(paragraphs: &[&str]) -> RbDoc {
1308
+ let doc = new_doc();
1309
+ for (n, words) in paragraphs.iter().enumerate() {
1310
+ paragraph(&doc, n as u32, words);
1311
+ }
1312
+ doc
1313
+ }
1314
+
1315
+ fn remove_child(doc: &RbDoc, at: u32) {
1316
+ root(doc).remove_range(&mut doc.0.transact_mut(), at, 1);
1317
+ }
1318
+
1319
+ #[ruby_test]
1320
+ fn a_doc_writes_under_the_client_id_it_was_given() {
1321
+ let doc = RbDoc::new(&[rb(42_u64)]).unwrap();
1322
+ assert_eq!(doc.client_id(), 42);
1323
+ assert_ne!(new_doc().client_id(), new_doc().client_id());
1324
+ }
1325
+
1326
+ #[ruby_test]
1327
+ fn an_update_carries_state_from_one_doc_to_another() {
1328
+ let doc = new_doc();
1329
+ push(&doc, "t", "hello");
1330
+ let peer = new_doc();
1331
+ peer.apply_update(update(&doc)).unwrap();
1332
+ assert_eq!(
1333
+ peer.read_text("t".into()).unwrap().as_deref(),
1334
+ Some("hello")
1335
+ );
1336
+ assert_eq!(
1337
+ copy_bytes(peer.encode_state_vector().unwrap()),
1338
+ copy_bytes(doc.encode_state_vector().unwrap())
1339
+ );
1340
+ }
1341
+
1342
+ #[ruby_test]
1343
+ fn an_update_since_a_state_vector_holds_only_what_is_new() {
1344
+ let doc = new_doc();
1345
+ push(&doc, "t", "one");
1346
+ let peer = new_doc();
1347
+ peer.apply_update(update(&doc)).unwrap();
1348
+ push(&doc, "t", " two");
1349
+ let diff = update_since(&doc, peer.encode_state_vector().unwrap());
1350
+ let fresh = new_doc();
1351
+ fresh.apply_update(diff).unwrap();
1352
+ assert!(
1353
+ fresh.pending().unwrap(),
1354
+ "the diff leaves out the first write"
1355
+ );
1356
+ peer.apply_update(diff).unwrap();
1357
+ assert_eq!(
1358
+ peer.read_text("t".into()).unwrap().as_deref(),
1359
+ Some("one two")
1360
+ );
1361
+ }
1362
+
1363
+ #[ruby_test]
1364
+ fn a_malformed_update_raises() {
1365
+ assert!(new_doc()
1366
+ .apply_update(binary_string(&[0xff, 0xff]))
1367
+ .is_err());
1368
+ }
1369
+
1370
+ #[ruby_test]
1371
+ fn reads_return_each_kind_of_root_and_nil_for_a_missing_one() {
1372
+ let doc = lexical_doc(&["one", "two"]);
1373
+ push(&doc, "t", "hi");
1374
+ doc.0.get_or_insert_map("m");
1375
+ let mut names = doc.root_names().unwrap();
1376
+ names.sort();
1377
+ assert_eq!(names, vec!["m", "root", "t"]);
1378
+ assert_eq!(doc.read_text("t".into()).unwrap().as_deref(), Some("hi"));
1379
+ assert_eq!(doc.read_map("m".into()).unwrap().as_deref(), Some("{}"));
1380
+ assert_eq!(
1381
+ doc.read_xml("root".into()).unwrap().as_deref(),
1382
+ Some("one\ntwo")
1383
+ );
1384
+ assert_eq!(doc.read_array("missing".into()).unwrap(), None);
1385
+ }
1386
+
1387
+ #[ruby_test]
1388
+ fn an_update_missing_its_dependency_is_pending_until_it_arrives() {
1389
+ let source = new_doc();
1390
+ push(&source, "t", "a");
1391
+ let first = update(&source);
1392
+ let after_first = source.encode_state_vector().unwrap();
1393
+ push(&source, "t", "b");
1394
+ let second = update_since(&source, after_first);
1395
+
1396
+ let doc = new_doc();
1397
+ assert!(!doc.update_ready(second).unwrap());
1398
+ assert!(doc.update_advances(second).unwrap());
1399
+ doc.apply_update(second).unwrap();
1400
+ assert!(doc.pending().unwrap());
1401
+ assert_eq!(doc.read_text("t".into()).unwrap(), None);
1402
+ let compacted = copy_bytes(doc.compacted_state_update().unwrap());
1403
+ assert!(compacted.len() < copy_bytes(update(&doc)).len());
1404
+
1405
+ doc.apply_update(first).unwrap();
1406
+ assert!(!doc.pending().unwrap());
1407
+ assert_eq!(doc.read_text("t".into()).unwrap().as_deref(), Some("ab"));
1408
+ assert!(!doc.update_advances(second).unwrap());
1409
+ }
1410
+
1411
+ #[ruby_test]
1412
+ fn a_sync_step1_answered_by_a_peer_brings_the_doc_up_to_date() {
1413
+ let doc = new_doc();
1414
+ push(&doc, "t", "hello");
1415
+ let peer = new_doc();
1416
+ let (kind, step, reply) = doc.handle_sync_message(peer.sync_step1().unwrap()).unwrap();
1417
+ assert_eq!((kind, step), (0, 0));
1418
+ let (kind, step, nothing) = peer.handle_sync_message(reply).unwrap();
1419
+ assert_eq!((kind, step), (0, 1));
1420
+ assert!(copy_bytes(nothing).is_empty());
1421
+ assert_eq!(
1422
+ peer.read_text("t".into()).unwrap().as_deref(),
1423
+ Some("hello")
1424
+ );
1425
+ }
1426
+
1427
+ #[ruby_test]
1428
+ fn handle_sync_message_applies_an_update_and_rejects_garbage() {
1429
+ let doc = new_doc();
1430
+ push(&doc, "t", "hi");
1431
+ let peer = new_doc();
1432
+ assert_eq!(
1433
+ peer.handle_sync_message(wrap_update(update(&doc)))
1434
+ .unwrap()
1435
+ .1,
1436
+ 2
1437
+ );
1438
+ assert_eq!(peer.read_text("t".into()).unwrap().as_deref(), Some("hi"));
1439
+ assert!(peer.handle_sync_message(binary_string(&[9, 9])).is_err());
1440
+ }
1441
+
1442
+ #[ruby_test]
1443
+ fn an_update_since_a_snapshot_holds_what_changed_after_it() {
1444
+ let doc = new_doc();
1445
+ push(&doc, "t", "before");
1446
+ let snapshot = doc.snapshot().unwrap();
1447
+ assert!(doc.update_since(snapshot).unwrap().is_none());
1448
+ push(&doc, "t", " after");
1449
+ let peer = new_doc();
1450
+ peer.apply_update(doc.update_since(snapshot).unwrap().unwrap())
1451
+ .unwrap();
1452
+ assert!(
1453
+ peer.pending().unwrap(),
1454
+ "the update leaves out what came before"
1455
+ );
1456
+ peer.apply_update(update(&doc)).unwrap();
1457
+ assert_eq!(
1458
+ peer.read_text("t".into()).unwrap().as_deref(),
1459
+ Some("before after")
1460
+ );
1461
+ }
1462
+
1463
+ #[ruby_test]
1464
+ fn an_update_since_a_snapshot_carries_only_new_deletions() {
1465
+ let doc = new_doc();
1466
+ let text = doc.0.get_or_insert_text("t");
1467
+ for _ in 0..200 {
1468
+ push(&doc, "t", "xy");
1469
+ let mut txn = doc.0.transact_mut();
1470
+ let at = text.len(&txn) - 1;
1471
+ text.remove_range(&mut txn, at, 1);
1472
+ }
1473
+ let snapshot = doc.snapshot().unwrap();
1474
+ let peer = new_doc();
1475
+ peer.apply_update(update(&doc)).unwrap();
1476
+ text.remove_range(&mut doc.0.transact_mut(), 0, 1);
1477
+ let chunk = doc.update_since(snapshot).unwrap().unwrap();
1478
+ assert!(
1479
+ copy_bytes(chunk).len() < 40,
1480
+ "{} bytes",
1481
+ copy_bytes(chunk).len()
1482
+ );
1483
+ peer.apply_update(chunk).unwrap();
1484
+ assert_eq!(
1485
+ peer.read_text("t".into()).unwrap(),
1486
+ doc.read_text("t".into()).unwrap()
1487
+ );
1488
+ }
1489
+
1490
+ #[ruby_test]
1491
+ fn apply_update_changes_reports_the_blocks_an_update_touched() {
1492
+ let doc = lexical_doc(&["one", "two", "three"]);
1493
+ let peer = new_doc();
1494
+ peer.apply_update(update(&doc)).unwrap();
1495
+ let before = peer.encode_state_vector().unwrap();
1496
+ paragraph(&peer, 1, "new");
1497
+ remove_child(&peer, 3);
1498
+ let changed = doc
1499
+ .apply_update_changes(update_since(&peer, before), "root".into())
1500
+ .unwrap();
1501
+ assert_eq!(changed.as_value().inspect(), "[1, 2]");
1502
+ assert_eq!(
1503
+ doc.read_xml("root".into()).unwrap().as_deref(),
1504
+ Some("one\nnew\ntwo")
1505
+ );
1506
+ }
1507
+
1508
+ #[ruby_test]
1509
+ fn a_block_position_finds_its_block_as_others_come_and_go() {
1510
+ let doc = lexical_doc(&["one", "two"]);
1511
+ let ruby = Ruby::get().unwrap();
1512
+ let root = root(&doc);
1513
+ let position = {
1514
+ let txn = doc.0.transact();
1515
+ let blocks = shared::embedded_xml_texts(&txn, &root);
1516
+ let sticky = yrs::StickyIndex::from_type(&txn, &blocks[1], yrs::Assoc::After);
1517
+ shared::Position::new(sticky).to_ruby(&ruby).unwrap()
1518
+ };
1519
+ paragraph(&doc, 0, "zero");
1520
+ assert_eq!(doc.block_at(position, "root".into()).unwrap(), Some(2));
1521
+ let found = doc.find(position, "root".into()).unwrap();
1522
+ assert!(found.is_some());
1523
+ assert!(doc.find(position, "elsewhere".into()).unwrap().is_none());
1524
+ remove_child(&doc, 2);
1525
+ assert!(doc.find(position, "root".into()).unwrap().is_none());
1526
+ }
1527
+
1528
+ #[ruby_test]
1529
+ fn children_count_blocks_but_not_other_embeds() {
1530
+ let doc = lexical_doc(&["one"]);
1531
+ root(&doc).insert_embed(&mut doc.0.transact_mut(), 0, Any::from(true));
1532
+ paragraph(&doc, 2, "two");
1533
+ let root = root(&doc);
1534
+ let txn = doc.0.transact();
1535
+ let children = Children::of(&txn, &root);
1536
+ assert!(!children.is_block(0));
1537
+ assert!(children.is_block(1));
1538
+ assert_eq!(children.ordinal(2), 1);
1539
+ assert_eq!(children.blocks_in(0, 3), 2);
1540
+ }
1541
+
1542
+ #[ruby_test]
1543
+ fn awareness_frames_carry_presence_between_clients() {
1544
+ let agent = RbAwareness::new(&[rb(7_u64)]).unwrap();
1545
+ let browser = RbAwareness::new(&[]).unwrap();
1546
+ assert_eq!(agent.client_id(), 7);
1547
+ let frame = agent.set_local_state(r#"{"name":"Agent"}"#.into()).unwrap();
1548
+ assert!(browser.apply_update(frame).unwrap());
1549
+ let states = browser.states().unwrap().as_value().inspect();
1550
+ assert!(states.contains(r#"7 => {"name" => "Agent"}"#), "{states}");
1551
+ assert!(!browser.clocks().unwrap().is_empty());
1552
+
1553
+ assert!(browser
1554
+ .apply_update(agent.clear_local_state().unwrap())
1555
+ .unwrap());
1556
+ let states = browser.states().unwrap().as_value().inspect();
1557
+ assert!(!states.contains("Agent"), "{states}");
1558
+ }
1559
+
1560
+ #[ruby_test]
1561
+ fn awareness_refuses_a_state_that_is_not_json_and_ignores_other_frames() {
1562
+ let awareness = RbAwareness::new(&[]).unwrap();
1563
+ assert!(awareness.set_local_state("{nope".into()).is_err());
1564
+ assert!(!awareness
1565
+ .apply_update(new_doc().sync_step1().unwrap())
1566
+ .unwrap());
1567
+ }
1568
+
1569
+ #[ruby_test]
1570
+ fn the_lexical_renderer_renders_a_lexical_doc_and_skips_a_missing_root() {
1571
+ let doc = lexical_doc(&["hello"]);
1572
+ let lexical = RbLexical::native_new(&doc, "{}".into()).unwrap();
1573
+ assert_eq!(
1574
+ lexical.native_to_html(&[]).unwrap().inspect(),
1575
+ r#""<p>hello</p>""#
1576
+ );
1577
+ let types = lexical.node_types(&[]).unwrap().inspect();
1578
+ assert!(types.contains("paragraph"), "{types}");
1579
+ assert!(lexical.native_to_html(&[rb("missing")]).unwrap().is_nil());
1580
+ assert!(lexical.native_to_html(&[rb("a"), rb("b")]).is_err());
1581
+ }
1582
+
1583
+ #[ruby_test]
1584
+ fn the_prosemirror_renderer_skips_a_missing_root_and_refuses_bad_rules() {
1585
+ let doc = new_doc();
1586
+ let prosemirror = RbProseMirror::native_new(&doc, "{}".into()).unwrap();
1587
+ assert!(prosemirror.native_to_html(&[]).unwrap().is_nil());
1588
+ assert!(prosemirror.node_types(&[]).unwrap().is_nil());
1589
+ assert!(RbProseMirror::native_new(&doc, "not json".into()).is_err());
1590
+ }
1591
+ }