yrby 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc870d86089bc5373cbac29455b1b7818c4a710b4ca1c8c12eeee2939faa8758
4
- data.tar.gz: 66c41fad450c8e71a23a166bc1f0e6a83def6bdcc94c79724bb63d7691a28f16
3
+ metadata.gz: b872650c1daa2696dc90e8f209b3dba702d7bb796f5af330550343a7e7397e51
4
+ data.tar.gz: 9070c8d98f2323f80ea60e79d59c307a47486465093c384182252136faab69d0
5
5
  SHA512:
6
- metadata.gz: 0725a1a0966f516316041b3d46edcd946783802691d568ed139549fbc120515b809ca368784f23f522bad7e0a4f24abb46951985ff0935627b1733d2e3b47cd3
7
- data.tar.gz: 6fcf5914510b36d10fe3bf344725cb151f945c57df95b692cdd5a4344ceeba2e81ed6455a0ec3b4071ae5a9c569de4bc0ae3bea9aa6b40b399594c296fb71a0b
6
+ metadata.gz: b3d3d9536640200254a1b66bba0e7a3090215ba8d25b614c9e7908d4ef3cec02f0a31115608c4f9b108636735a1eff42acd39b44519b6b03e0d362b66df73b29
7
+ data.tar.gz: f77e3b5693746f19a4707c5f2a3d31d2b45d83122fc6d5faf362c4a11b2b52b4f7b712f0433aee38bf5b8fee352d0bd9539012b5e74bc43fd81f6d73616a4974
data/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.1] - 2026-11-08
10
+
11
+ ### Changed
12
+
13
+ - yrby builds against yrs 0.28.1. An insert into a text no longer recounts
14
+ the UTF-16 length of each string before it, so appending to a long text
15
+ costs about the same at any length: 80k one-character appends took 2.5 s
16
+ and now take 48 ms.
17
+
18
+ ### Experimental
19
+
20
+ - `Y::Doc#edit` returns the document's own writes and never a peer's. The
21
+ document keeps the update of every local transaction until an edit returns
22
+ it, merged into one, so a write made outside an edit, from another thread
23
+ or fiber, or in a block that raised comes with the next edit. Before this,
24
+ the edit returned everything the document gained while the block ran, so
25
+ an update applied from a peer meanwhile was sent again as the edit's own,
26
+ and a caller had to lock the document to keep them apart. Each edit takes
27
+ everything unsent at once, so the updates edits return have no gaps
28
+ between them. A peer's update doesn't wait for an edit.
29
+
30
+ - `Y::Map#set_text(key, content = "")` stores a new `Y.Text` at a key and
31
+ returns its live `Y::Text`. `Y::Array#push_text(content = "")` and
32
+ `#insert_text(index, content = "")` do the same in a list. Before this,
33
+ Ruby could edit a nested `Y.Text` a browser made but couldn't make one,
34
+ so a server couldn't set up a form whose fields merge typing.
35
+
9
36
  ## yrby-client 0.7.1 - 2026-11-03
10
37
 
11
38
  ### Fixed
data/Cargo.lock CHANGED
@@ -44,16 +44,14 @@ dependencies = [
44
44
 
45
45
  [[package]]
46
46
  name = "bindgen"
47
- version = "0.69.5"
47
+ version = "0.72.1"
48
48
  source = "registry+https://github.com/rust-lang/crates.io-index"
49
- checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
49
+ checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
50
50
  dependencies = [
51
51
  "bitflags",
52
52
  "cexpr",
53
53
  "clang-sys",
54
54
  "itertools",
55
- "lazy_static",
56
- "lazycell",
57
55
  "proc-macro2",
58
56
  "quote",
59
57
  "regex",
@@ -221,12 +219,6 @@ version = "1.5.0"
221
219
  source = "registry+https://github.com/rust-lang/crates.io-index"
222
220
  checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
223
221
 
224
- [[package]]
225
- name = "lazycell"
226
- version = "1.3.0"
227
- source = "registry+https://github.com/rust-lang/crates.io-index"
228
- checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
229
-
230
222
  [[package]]
231
223
  name = "lexical-yjs-html"
232
224
  version = "0.2.1"
@@ -366,18 +358,18 @@ dependencies = [
366
358
 
367
359
  [[package]]
368
360
  name = "rb-sys"
369
- version = "0.9.124"
361
+ version = "0.9.130"
370
362
  source = "registry+https://github.com/rust-lang/crates.io-index"
371
- checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
363
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
372
364
  dependencies = [
373
365
  "rb-sys-build",
374
366
  ]
375
367
 
376
368
  [[package]]
377
369
  name = "rb-sys-build"
378
- version = "0.9.124"
370
+ version = "0.9.130"
379
371
  source = "registry+https://github.com/rust-lang/crates.io-index"
380
- checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
372
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
381
373
  dependencies = [
382
374
  "bindgen",
383
375
  "lazy_static",
@@ -461,9 +453,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
461
453
 
462
454
  [[package]]
463
455
  name = "rustc-hash"
464
- version = "1.1.0"
456
+ version = "2.1.3"
465
457
  source = "registry+https://github.com/rust-lang/crates.io-index"
466
- checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
458
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
467
459
 
468
460
  [[package]]
469
461
  name = "rustversion"
@@ -549,9 +541,9 @@ dependencies = [
549
541
 
550
542
  [[package]]
551
543
  name = "smallvec"
552
- version = "1.15.1"
544
+ version = "1.16.4"
553
545
  source = "registry+https://github.com/rust-lang/crates.io-index"
554
- checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
546
+ checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
555
547
 
556
548
  [[package]]
557
549
  name = "syn"
@@ -672,9 +664,9 @@ dependencies = [
672
664
 
673
665
  [[package]]
674
666
  name = "yrs"
675
- version = "0.28.0"
667
+ version = "0.28.1"
676
668
  source = "registry+https://github.com/rust-lang/crates.io-index"
677
- checksum = "52c70dc8beca8666c77612a96889106ca3cd65318609721f464624ff79685da9"
669
+ checksum = "9f7716b4846f9fab489af2ed7ab31c4554f5fe6965ede2392b9cdfed0e5087ff"
678
670
  dependencies = [
679
671
  "arc-swap",
680
672
  "async-lock",
data/ext/yrby/Cargo.toml CHANGED
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
12
12
  [dependencies]
13
13
  magnus = "0.8"
14
14
  rb-sys = "0.9"
15
- yrs = { version = "0.28", features = ["sync"] }
15
+ yrs = { version = "0.28.1", features = ["sync"] }
16
16
  serde_json = "1.0"
17
17
  yjs-html-core = { path = "crates/html-core" }
18
18
  lexical-yjs-html = { path = "crates/lexical-html" }
@@ -14,14 +14,14 @@
14
14
 
15
15
  use magnus::{prelude::*, typed_data::Obj, Error, Ruby, Value};
16
16
  use yrs::branch::BranchID;
17
- use yrs::{Any, Array, ArrayRef, Doc, Transact};
17
+ use yrs::{Any, Array, ArrayRef, Doc, TextPrelim, Transact};
18
18
 
19
19
  use crate::map::RbMap;
20
20
  use crate::nogvl;
21
21
  use crate::read::out_to_any;
22
22
  use crate::shared::{
23
- any_to_ruby, element_index, guarded, identity, insert_index, resolve, ruby_to_invalue, to_in,
24
- Child, ORIGIN,
23
+ any_to_ruby, element_index, identity, insert_index, resolve, ruby_to_invalue, to_in, write,
24
+ Child,
25
25
  };
26
26
  use crate::text::RbText;
27
27
 
@@ -162,29 +162,39 @@ impl RbArray {
162
162
  let ruby = Ruby::get().unwrap();
163
163
  let iv = ruby_to_invalue(&ruby, value)?;
164
164
  let (doc, id) = (&self.doc, &self.id);
165
- guarded(move || {
166
- let mut txn = doc.transact_mut_with(ORIGIN);
167
- let a = resolve::<ArrayRef, _>(&txn, id).ok_or("array no longer exists")?;
168
- let at = insert_index(index, a.len(&txn));
169
- a.insert(&mut txn, at, to_in(iv));
165
+ write(doc, move |txn| {
166
+ let a = resolve::<ArrayRef, _>(txn, id).ok_or("array no longer exists")?;
167
+ let at = insert_index(index, a.len(txn));
168
+ a.insert(txn, at, to_in(iv));
170
169
  Ok(())
171
170
  })
172
171
  }
173
172
 
173
+ /// Insert a new `Y.Text` holding `content` at `index` (the same index
174
+ /// rules as `insert`) and return a live handle to it.
175
+ fn insert_text(&self, index: i64, content: String) -> Result<RbText, Error> {
176
+ let (doc, id) = (&self.doc, &self.id);
177
+ let text = write(doc, move |txn| {
178
+ let a = resolve::<ArrayRef, _>(txn, id).ok_or("array no longer exists")?;
179
+ let at = insert_index(index, a.len(txn));
180
+ Ok(identity(&a.insert(txn, at, TextPrelim::new(content))))
181
+ })?;
182
+ Ok(RbText::new(self.doc.clone(), text))
183
+ }
184
+
174
185
  /// Remove the element at `index`, returning its previous snapshot value.
175
186
  fn delete_at(&self, index: i64) -> Result<Value, Error> {
176
187
  let (doc, id) = (&self.doc, &self.id);
177
- let prev: Option<Any> = guarded(move || {
178
- let mut txn = doc.transact_mut_with(ORIGIN);
179
- let Some(a) = resolve::<ArrayRef, _>(&txn, id) else {
188
+ let prev: Option<Any> = write(doc, move |txn| {
189
+ let Some(a) = resolve::<ArrayRef, _>(txn, id) else {
180
190
  return Ok(None);
181
191
  };
182
- let Some(i) = element_index(index, a.len(&txn)) else {
192
+ let Some(i) = element_index(index, a.len(txn)) else {
183
193
  return Ok(None);
184
194
  };
185
195
  // Convert before removing: a removed shared ref would dangle.
186
- let prev = a.get(&txn, i).map(|v| out_to_any(&txn, &v));
187
- a.remove(&mut txn, i);
196
+ let prev = a.get(txn, i).map(|v| out_to_any(txn, &v));
197
+ a.remove(txn, i);
188
198
  Ok(prev)
189
199
  })?;
190
200
  let ruby = Ruby::get().unwrap();
@@ -196,12 +206,11 @@ impl RbArray {
196
206
 
197
207
  fn clear(&self) -> Result<(), Error> {
198
208
  let (doc, id) = (&self.doc, &self.id);
199
- guarded(move || {
200
- let mut txn = doc.transact_mut_with(ORIGIN);
201
- if let Some(a) = resolve::<ArrayRef, _>(&txn, id) {
202
- let len = a.len(&txn);
209
+ write(doc, move |txn| {
210
+ if let Some(a) = resolve::<ArrayRef, _>(txn, id) {
211
+ let len = a.len(txn);
203
212
  if len > 0 {
204
- a.remove_range(&mut txn, 0, len);
213
+ a.remove_range(txn, 0, len);
205
214
  }
206
215
  }
207
216
  Ok(())
@@ -215,12 +224,11 @@ impl RbArray {
215
224
  let ruby = Ruby::get().unwrap();
216
225
  let iv = ruby_to_invalue(&ruby, value)?;
217
226
  let (doc, id) = (&self.doc, &self.id);
218
- guarded(move || {
219
- let mut txn = doc.transact_mut_with(ORIGIN);
220
- let a = resolve::<ArrayRef, _>(&txn, id).ok_or("array no longer exists")?;
221
- let i = element_index(index, a.len(&txn)).ok_or("index out of range")?;
222
- a.remove(&mut txn, i);
223
- a.insert(&mut txn, i, to_in(iv));
227
+ write(doc, move |txn| {
228
+ let a = resolve::<ArrayRef, _>(txn, id).ok_or("array no longer exists")?;
229
+ let i = element_index(index, a.len(txn)).ok_or("index out of range")?;
230
+ a.remove(txn, i);
231
+ a.insert(txn, i, to_in(iv));
224
232
  Ok(())
225
233
  })?;
226
234
  Ok(value)
@@ -246,6 +254,10 @@ pub fn define(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
246
254
  class.define_method("push", magnus::method!(RbArray::push, 1))?;
247
255
  class.define_method("<<", magnus::method!(RbArray::push, 1))?;
248
256
  class.define_method("insert", magnus::method!(RbArray::insert, 2))?;
257
+ class.define_method(
258
+ "native_insert_text",
259
+ magnus::method!(RbArray::insert_text, 2),
260
+ )?;
249
261
  class.define_method("delete_at", magnus::method!(RbArray::delete_at, 1))?;
250
262
  class.define_method("clear", magnus::method!(RbArray::clear, 0))?;
251
263
  class.define_method("size", magnus::method!(RbArray::size, 0))?;
data/ext/yrby/src/lib.rs CHANGED
@@ -33,7 +33,49 @@ pub(crate) use yjs_html_core as render_rules;
33
33
  /// interior-mutability wrapper (RefCell and friends): every method opens and
34
34
  /// closes its transaction within a single call.
35
35
  #[magnus::wrap(class = "Y::Doc", free_immediately, size)]
36
- struct RbDoc(Doc);
36
+ struct RbDoc(Doc, Unsent);
37
+
38
+ /// The updates of this document's own transactions that no `Doc#edit` has
39
+ /// returned yet. Every transaction but a peer's lands here when it commits,
40
+ /// after yrs is done changing the document, whichever thread or fiber made
41
+ /// it and whether or not an edit was running. So no local write is left out.
42
+ /// They're merged only when an edit takes them, so until then the document
43
+ /// holds the bytes of every local write.
44
+ type Unsent = std::sync::Arc<std::sync::Mutex<Vec<Vec<u8>>>>;
45
+
46
+ /// `Unsent`'s list. A push or a take holds it only briefly, so taking it
47
+ /// with the GVL held never waits long.
48
+ fn lock(unsent: &Unsent) -> std::sync::MutexGuard<'_, Vec<Vec<u8>>> {
49
+ unsent
50
+ .lock()
51
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
52
+ }
53
+
54
+ /// How many updates `merge_unsent` merges at once.
55
+ const MERGE_FAN_IN: usize = 16;
56
+
57
+ /// The updates merged into one, a few at a time and then the results again.
58
+ /// One merge of them all costs time quadratic in how many there are.
59
+ fn merge_unsent(updates: &[Vec<u8>]) -> Result<Vec<u8>, String> {
60
+ let mut level: Vec<Vec<u8>> = match updates {
61
+ [update] => return Ok(update.clone()),
62
+ _ => merge_chunks(updates)?,
63
+ };
64
+ while level.len() > 1 {
65
+ level = merge_chunks(&level)?;
66
+ }
67
+ Ok(level.pop().unwrap_or_default())
68
+ }
69
+
70
+ fn merge_chunks(updates: &[Vec<u8>]) -> Result<Vec<Vec<u8>>, String> {
71
+ updates
72
+ .chunks(MERGE_FAN_IN)
73
+ .map(|chunk| match chunk {
74
+ [update] => Ok(update.clone()),
75
+ _ => yrs::merge_updates_v1(chunk).map_err(|e| e.to_string()),
76
+ })
77
+ .collect()
78
+ }
37
79
 
38
80
  /// Compile-time proof that the wrapped Doc is thread-safe. If a future yrs
39
81
  /// upgrade makes Doc lose Send/Sync, this fails the build instead of silently
@@ -171,7 +213,17 @@ impl RbDoc {
171
213
  yrs::Options::with_client_id(yrs::block::ClientID::new(client_id))
172
214
  };
173
215
  options.offset_kind = yrs::OffsetKind::Utf16;
174
- Ok(RbDoc(Doc::with_options(options)))
216
+ let doc = Doc::with_options(options);
217
+ let unsent = Unsent::default();
218
+ let sink = unsent.clone();
219
+ doc.observe_update_v1("yrby-unsent", move |txn, event| {
220
+ if shared::is_remote(txn.origin()) {
221
+ return;
222
+ }
223
+ lock(&sink).push(event.update.clone());
224
+ })
225
+ .map_err(|e| yrb_error(e.to_string()))?;
226
+ Ok(RbDoc(doc, unsent))
175
227
  }
176
228
 
177
229
  /// The id this document writes its own edits under. Presence for the same
@@ -312,7 +364,7 @@ impl RbDoc {
312
364
  // One transaction from the snapshot of the blocks to the commit,
313
365
  // under an origin of its own: another write can't change the
314
366
  // blocks in between, and its events don't count.
315
- let key = shared::observer_key();
367
+ let key = shared::remote_key();
316
368
  let mut txn = doc.transact_mut_with(key.clone());
317
369
  let fragment = yrs::WriteTxn::get_or_insert_xml_fragment(&mut txn, root.as_str());
318
370
  let branch: &yrs::branch::Branch = fragment.as_ref();
@@ -398,30 +450,31 @@ impl RbDoc {
398
450
  Ok(id.map(|id| xml_text::handle(&self.0, id, root)))
399
451
  }
400
452
 
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))
453
+ /// Every update in `Unsent`, merged into one, or nil for none. Taking them
454
+ /// empties it, and writes made meanwhile wait for the next take. The
455
+ /// merge runs without the GVL. If it fails, or an interrupt comes back
456
+ /// with it (a signal trap can raise even where `Thread.handle_interrupt`
457
+ /// masks), the updates go back ahead of those writes.
458
+ fn take_unsent(&self) -> Result<Option<RString>, Error> {
459
+ let taken = std::mem::take(&mut *lock(&self.1));
460
+ if taken.is_empty() {
461
+ return Ok(None);
462
+ }
463
+ match nogvl(|| merge_unsent(&taken)).and_then(|merged| merged.map_err(yrb_error)) {
464
+ Ok(merged) => Ok(Some(binary_string(&merged))),
465
+ Err(error) => {
466
+ let mut unsent = lock(&self.1);
467
+ let since = std::mem::replace(&mut *unsent, taken);
468
+ unsent.extend(since);
469
+ Err(error)
470
+ }
471
+ }
407
472
  }
408
473
 
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)))
474
+ /// Put a taken update back ahead of anything written since, for the next
475
+ /// take to return again.
476
+ fn keep_unsent(&self, update: RString) {
477
+ lock(&self.1).insert(0, copy_bytes(update));
425
478
  }
426
479
 
427
480
  /// Encode state as update (optionally diffed against a state vector)
@@ -450,7 +503,7 @@ impl RbDoc {
450
503
  let doc = &self.0;
451
504
  nogvl(move || -> Result<(), String> {
452
505
  let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
453
- let mut txn = doc.transact_mut();
506
+ let mut txn = doc.transact_mut_with(shared::REMOTE);
454
507
  txn.apply_update(update).map_err(|e| e.to_string())
455
508
  })?
456
509
  .map_err(yrb_error)
@@ -513,7 +566,7 @@ impl RbDoc {
513
566
  // Apply the update
514
567
  let update =
515
568
  yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
516
- let mut txn = doc.transact_mut();
569
+ let mut txn = doc.transact_mut_with(shared::REMOTE);
517
570
  txn.apply_update(update).map_err(|e| e.to_string())?;
518
571
  Ok((0, 1, Vec::new()))
519
572
  }
@@ -521,7 +574,7 @@ impl RbDoc {
521
574
  // Apply the update
522
575
  let update =
523
576
  yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
524
- let mut txn = doc.transact_mut();
577
+ let mut txn = doc.transact_mut_with(shared::REMOTE);
525
578
  txn.apply_update(update).map_err(|e| e.to_string())?;
526
579
  Ok((0, 2, Vec::new()))
527
580
  }
@@ -805,27 +858,6 @@ fn update_from_message(data: RString) -> Result<Option<RString>, Error> {
805
858
  // Module Initialization
806
859
  // ============================================================================
807
860
 
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
861
  /// A root's children in order: the document index each starts at and
830
862
  /// whether it is an `XmlText` block. Ordinals count only those, as
831
863
  /// `xml_text(n)` does; a divider or an image is a child but not a block.
@@ -1182,8 +1214,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1182
1214
  )?;
1183
1215
  doc_class.define_private_method("native_block_at", method!(RbDoc::block_at, 2))?;
1184
1216
  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))?;
1217
+ doc_class.define_private_method("native_take_unsent", method!(RbDoc::take_unsent, 0))?;
1218
+ doc_class.define_private_method("native_keep_unsent", method!(RbDoc::keep_unsent, 1))?;
1187
1219
  doc_class.define_method("root_names", method!(RbDoc::root_names, 0))?;
1188
1220
  doc_class.define_method("read_text", method!(RbDoc::read_text, 1))?;
1189
1221
  doc_class.define_method("read_xml", method!(RbDoc::read_xml, 1))?;
@@ -1360,6 +1392,69 @@ mod tests {
1360
1392
  );
1361
1393
  }
1362
1394
 
1395
+ fn take(doc: &RbDoc) -> Option<RString> {
1396
+ doc.take_unsent().unwrap()
1397
+ }
1398
+
1399
+ #[ruby_test]
1400
+ fn local_writes_wait_to_be_taken_and_a_peers_never_do() {
1401
+ let doc = new_doc();
1402
+ push(&doc, "t", "mine");
1403
+ let mine = take(&doc).expect("a local write waits");
1404
+ assert!(take(&doc).is_none(), "taking empties it");
1405
+
1406
+ let peer = new_doc();
1407
+ peer.apply_update(mine).unwrap();
1408
+ peer.handle_sync_message(wrap_update(mine)).unwrap();
1409
+ peer.apply_update_changes(mine, "root".into()).unwrap();
1410
+ assert!(take(&peer).is_none(), "applied updates are a peer's");
1411
+ assert_eq!(peer.read_text("t".into()).unwrap().as_deref(), Some("mine"));
1412
+ }
1413
+
1414
+ #[ruby_test]
1415
+ fn unsent_updates_merge_into_one_that_carries_them_all() {
1416
+ let doc = new_doc();
1417
+ for n in 0..300 {
1418
+ push(&doc, "t", &n.to_string());
1419
+ }
1420
+ let fresh = new_doc();
1421
+ fresh.apply_update(take(&doc).unwrap()).unwrap();
1422
+ assert_eq!(
1423
+ fresh.read_text("t".into()).unwrap(),
1424
+ doc.read_text("t".into()).unwrap()
1425
+ );
1426
+ assert!(!fresh.pending().unwrap());
1427
+ }
1428
+
1429
+ #[ruby_test]
1430
+ fn a_kept_update_is_taken_again_with_what_came_after() {
1431
+ let doc = new_doc();
1432
+ push(&doc, "t", "inner");
1433
+ doc.keep_unsent(take(&doc).unwrap());
1434
+ push(&doc, "t", " outer");
1435
+ let fresh = new_doc();
1436
+ fresh.apply_update(take(&doc).unwrap()).unwrap();
1437
+ assert_eq!(
1438
+ fresh.read_text("t".into()).unwrap().as_deref(),
1439
+ Some("inner outer")
1440
+ );
1441
+ }
1442
+
1443
+ #[ruby_test]
1444
+ fn unsent_updates_stay_when_the_merge_fails() {
1445
+ let doc = new_doc();
1446
+ push(&doc, "t", "kept");
1447
+ doc.1.lock().unwrap().push(vec![255]);
1448
+ assert!(doc.take_unsent().is_err());
1449
+ doc.1.lock().unwrap().pop();
1450
+ let fresh = new_doc();
1451
+ fresh.apply_update(take(&doc).unwrap()).unwrap();
1452
+ assert_eq!(
1453
+ fresh.read_text("t".into()).unwrap().as_deref(),
1454
+ Some("kept")
1455
+ );
1456
+ }
1457
+
1363
1458
  #[ruby_test]
1364
1459
  fn a_malformed_update_raises() {
1365
1460
  assert!(new_doc()
@@ -1439,54 +1534,6 @@ mod tests {
1439
1534
  assert!(peer.handle_sync_message(binary_string(&[9, 9])).is_err());
1440
1535
  }
1441
1536
 
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
1537
  #[ruby_test]
1491
1538
  fn apply_update_changes_reports_the_blocks_an_update_touched() {
1492
1539
  let doc = lexical_doc(&["one", "two", "three"]);
data/ext/yrby/src/map.rs CHANGED
@@ -11,13 +11,13 @@
11
11
 
12
12
  use magnus::{prelude::*, typed_data::Obj, Error, Ruby, Value};
13
13
  use yrs::branch::BranchID;
14
- use yrs::{Any, Doc, Map, MapRef, Transact};
14
+ use yrs::{Any, Doc, Map, MapRef, TextPrelim, Transact};
15
15
 
16
16
  use crate::array::RbArray;
17
17
  use crate::nogvl;
18
18
  use crate::read::out_to_any;
19
19
  use crate::shared::{
20
- any_to_ruby, guarded, identity, key_to_string, resolve, ruby_to_invalue, to_in, Child, ORIGIN,
20
+ any_to_ruby, identity, key_to_string, resolve, ruby_to_invalue, to_in, write, Child,
21
21
  };
22
22
  use crate::text::RbText;
23
23
 
@@ -168,27 +168,38 @@ impl RbMap {
168
168
  let key = key_to_string(key)?;
169
169
  let iv = ruby_to_invalue(&ruby, value)?;
170
170
  let (doc, id) = (&self.doc, &self.id);
171
- guarded(move || {
172
- let mut txn = doc.transact_mut_with(ORIGIN);
173
- let m = resolve::<MapRef, _>(&txn, id).ok_or("map no longer exists")?;
174
- m.insert(&mut txn, key, to_in(iv));
171
+ write(doc, move |txn| {
172
+ let m = resolve::<MapRef, _>(txn, id).ok_or("map no longer exists")?;
173
+ m.insert(txn, key, to_in(iv));
175
174
  Ok(())
176
175
  })?;
177
176
  Ok(value)
178
177
  }
179
178
 
179
+ /// Store a new `Y.Text` holding `content` at `key`, replacing what was
180
+ /// there, and return a live handle to it. A text in a map is what lets
181
+ /// two people type in the same field and both keep their edit.
182
+ fn set_text(&self, key: Value, content: String) -> Result<RbText, Error> {
183
+ let key = key_to_string(key)?;
184
+ let (doc, id) = (&self.doc, &self.id);
185
+ let text = write(doc, move |txn| {
186
+ let m = resolve::<MapRef, _>(txn, id).ok_or("map no longer exists")?;
187
+ Ok(identity(&m.insert(txn, key, TextPrelim::new(content))))
188
+ })?;
189
+ Ok(RbText::new(self.doc.clone(), text))
190
+ }
191
+
180
192
  /// Remove `key`, returning its previous snapshot value (or `nil`).
181
193
  fn delete(&self, key: Value) -> Result<Value, Error> {
182
194
  let key = key_to_string(key)?;
183
195
  let (doc, id) = (&self.doc, &self.id);
184
- let prev: Option<Any> = guarded(move || {
185
- let mut txn = doc.transact_mut_with(ORIGIN);
186
- let Some(m) = resolve::<MapRef, _>(&txn, id) else {
196
+ let prev: Option<Any> = write(doc, move |txn| {
197
+ let Some(m) = resolve::<MapRef, _>(txn, id) else {
187
198
  return Ok(None);
188
199
  };
189
200
  // Convert before removing (a removed shared ref would dangle).
190
- let prev = m.get(&txn, &key).map(|v| out_to_any(&txn, &v));
191
- m.remove(&mut txn, &key);
201
+ let prev = m.get(txn, &key).map(|v| out_to_any(txn, &v));
202
+ m.remove(txn, &key);
192
203
  Ok(prev)
193
204
  })?;
194
205
  let ruby = Ruby::get().unwrap();
@@ -200,10 +211,9 @@ impl RbMap {
200
211
 
201
212
  fn clear(&self) -> Result<(), Error> {
202
213
  let (doc, id) = (&self.doc, &self.id);
203
- guarded(move || {
204
- let mut txn = doc.transact_mut_with(ORIGIN);
205
- if let Some(m) = resolve::<MapRef, _>(&txn, id) {
206
- m.clear(&mut txn);
214
+ write(doc, move |txn| {
215
+ if let Some(m) = resolve::<MapRef, _>(txn, id) {
216
+ m.clear(txn);
207
217
  }
208
218
  Ok(())
209
219
  })
@@ -227,6 +237,7 @@ pub fn define(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
227
237
  class.define_method("get_text", magnus::method!(RbMap::get_text, 1))?;
228
238
  class.define_method("[]=", magnus::method!(RbMap::set, 2))?;
229
239
  class.define_method("set", magnus::method!(RbMap::set, 2))?;
240
+ class.define_method("native_set_text", magnus::method!(RbMap::set_text, 2))?;
230
241
  class.define_method("delete", magnus::method!(RbMap::delete, 1))?;
231
242
  class.define_method("clear", magnus::method!(RbMap::clear, 0))?;
232
243
  class.define_method("key?", magnus::method!(RbMap::has_key, 1))?;
@@ -24,26 +24,34 @@ use magnus::{
24
24
  use yrs::branch::{Branch, BranchID, BranchPtr};
25
25
  use yrs::types::text::YChange;
26
26
  use yrs::{
27
- Any, Hook, In, IndexScope, MapPrelim, Number, Out, ReadTxn, SharedRef, StickyIndex, Text, Xml,
28
- XmlOut, XmlTextRef,
27
+ Any, Doc, Hook, In, IndexScope, MapPrelim, Number, Out, ReadTxn, SharedRef, StickyIndex, Text,
28
+ Transact, TransactionMut, Xml, XmlOut, XmlTextRef,
29
29
  };
30
30
 
31
31
  use crate::{nogvl, yrb_error};
32
32
 
33
- /// The origin of every transaction a handle opens. `Doc#edit` returns only
34
- /// these, so an update applied from elsewhere while it runs stays out.
33
+ /// The origin of every transaction a handle opens, all through `write`.
35
34
  pub const ORIGIN: &str = "yrby";
36
35
 
36
+ /// What the origin of every transaction that applies a peer's update starts
37
+ /// with. The document keeps every other transaction's update for `Doc#edit`.
38
+ pub const REMOTE: &str = "yrby-remote";
39
+
40
+ /// True when `origin` marks a transaction that applied a peer's update.
41
+ pub fn is_remote(origin: Option<&yrs::Origin>) -> bool {
42
+ origin.is_some_and(|origin| origin.as_ref().starts_with(REMOTE.as_bytes()))
43
+ }
44
+
37
45
  /// The largest integer a double represents exactly (2^53).
38
46
  const MAX_SAFE_INTEGER: u64 = 1 << 53;
39
47
 
40
- /// A key no other observer uses. yrs registers an observer under a key and
41
- /// removes it by that key, so two observers on one document need different
42
- /// ones.
43
- pub fn observer_key() -> yrs::Origin {
48
+ /// An origin for a transaction that applies a peer's update, and a key no
49
+ /// other observer uses: yrs registers an observer under a key and removes it
50
+ /// by that key, so two observers on one document need different ones.
51
+ pub fn remote_key() -> yrs::Origin {
44
52
  static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
45
- NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
46
- .into()
53
+ let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
54
+ format!("{REMOTE}:{n}").as_str().into()
47
55
  }
48
56
 
49
57
  /// The branch `id` names, if it exists and has not been deleted.
@@ -97,6 +105,17 @@ where
97
105
  .map_err(yrb_error)
98
106
  }
99
107
 
108
+ /// Run `f` in a write transaction under `ORIGIN`, as `guarded` runs its
109
+ /// closure. The document keeps the transaction's update for `Doc#edit` when
110
+ /// it commits (see `RbDoc::new`).
111
+ pub fn write<F, R>(doc: &Doc, f: F) -> Result<R, Error>
112
+ where
113
+ F: FnOnce(&mut TransactionMut) -> Result<R, String> + Send,
114
+ R: Send,
115
+ {
116
+ guarded(move || f(&mut doc.transact_mut_with(ORIGIN)))
117
+ }
118
+
100
119
  // --- indexes ---
101
120
 
102
121
  /// What `insert(index, ...)` means in a sequence of `len`, as in Ruby: a
data/ext/yrby/src/text.rs CHANGED
@@ -17,7 +17,7 @@ use yrs::{Assoc, Doc, GetString, IndexedSequence, StickyIndex, Text, TextRef, Tr
17
17
  use crate::nogvl;
18
18
  use crate::shared::{
19
19
  char_len, delete_span, end_or_units, guarded, identity, insert_index, resolve, to_chars,
20
- to_units, Position, ORIGIN,
20
+ to_units, write, Position,
21
21
  };
22
22
  use crate::yrb_error;
23
23
 
@@ -84,11 +84,10 @@ impl RbText {
84
84
  /// appends). Clamped into range.
85
85
  fn insert_at(&self, index: i64, text: String) -> Result<(), Error> {
86
86
  let (doc, id) = (&self.doc, &self.id);
87
- guarded(move || {
88
- let mut txn = doc.transact_mut_with(ORIGIN);
89
- let t = resolve::<TextRef, _>(&txn, id).ok_or("text no longer exists")?;
90
- let at = end_or_units(&txn, &t, index);
91
- t.insert(&mut txn, at, &text);
87
+ write(doc, move |txn| {
88
+ let t = resolve::<TextRef, _>(txn, id).ok_or("text no longer exists")?;
89
+ let at = end_or_units(txn, &t, index);
90
+ t.insert(txn, at, &text);
92
91
  Ok(())
93
92
  })
94
93
  }
@@ -137,13 +136,12 @@ impl RbText {
137
136
  /// negative index counts from the end, and the length stops at the end.
138
137
  fn delete(&self, index: i64, length: i64) -> Result<(), Error> {
139
138
  let (doc, id) = (&self.doc, &self.id);
140
- guarded(move || {
141
- let mut txn = doc.transact_mut_with(ORIGIN);
142
- let t = resolve::<TextRef, _>(&txn, id).ok_or("text no longer exists")?;
143
- if let Some((from, to)) = delete_span(index, length, char_len(&txn, &t)) {
144
- let (from, to) = (to_units(&txn, &t, from), to_units(&txn, &t, to));
139
+ write(doc, move |txn| {
140
+ let t = resolve::<TextRef, _>(txn, id).ok_or("text no longer exists")?;
141
+ if let Some((from, to)) = delete_span(index, length, char_len(txn, &t)) {
142
+ let (from, to) = (to_units(txn, &t, from), to_units(txn, &t, to));
145
143
  if to > from {
146
- t.remove_range(&mut txn, from, to - from);
144
+ t.remove_range(txn, from, to - from);
147
145
  }
148
146
  }
149
147
  Ok(())
@@ -152,12 +150,11 @@ impl RbText {
152
150
 
153
151
  fn clear(&self) -> Result<(), Error> {
154
152
  let (doc, id) = (&self.doc, &self.id);
155
- guarded(move || {
156
- let mut txn = doc.transact_mut_with(ORIGIN);
157
- if let Some(t) = resolve::<TextRef, _>(&txn, id) {
158
- let len = t.len(&txn);
153
+ write(doc, move |txn| {
154
+ if let Some(t) = resolve::<TextRef, _>(txn, id) {
155
+ let len = t.len(txn);
159
156
  if len > 0 {
160
- t.remove_range(&mut txn, 0, len);
157
+ t.remove_range(txn, 0, len);
161
158
  }
162
159
  }
163
160
  Ok(())
@@ -23,8 +23,8 @@ use crate::nogvl;
23
23
  use crate::shared::{
24
24
  any_to_ruby, block_text, char_len, delete_span, element_index, embedded_xml_text_index,
25
25
  embedded_xml_texts, end_or_units, ends_with_xml_text, guarded, hash_pairs, identity,
26
- insert_index, resolve, ruby_to_invalue, to_in, to_map_prelim, to_units, InValue, Position,
27
- MAX_NESTING, ORIGIN,
26
+ insert_index, resolve, ruby_to_invalue, to_in, to_map_prelim, to_units, write, InValue,
27
+ Position, MAX_NESTING,
28
28
  };
29
29
  use crate::yrb_error;
30
30
 
@@ -166,10 +166,9 @@ impl RbXmlText {
166
166
  let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
167
167
  let iv = ruby_to_invalue(&ruby, value)?;
168
168
  let (doc, id) = (&self.doc, &self.id);
169
- guarded(move || {
170
- let mut txn = doc.transact_mut_with(ORIGIN);
171
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
172
- x.insert_attribute(&mut txn, key, to_in(iv));
169
+ write(doc, move |txn| {
170
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
171
+ x.insert_attribute(txn, key, to_in(iv));
173
172
  Ok(())
174
173
  })?;
175
174
  Ok(value)
@@ -180,11 +179,10 @@ impl RbXmlText {
180
179
  fn insert(&self, index: i64, chunk: String) -> Result<String, Error> {
181
180
  let (doc, id) = (&self.doc, &self.id);
182
181
  let text = chunk.clone();
183
- guarded(move || {
184
- let mut txn = doc.transact_mut_with(ORIGIN);
185
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
186
- let at = end_or_units(&txn, &x, index);
187
- x.insert(&mut txn, at, &text);
182
+ write(doc, move |txn| {
183
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
184
+ let at = end_or_units(txn, &x, index);
185
+ x.insert(txn, at, &text);
188
186
  Ok(())
189
187
  })?;
190
188
  Ok(chunk)
@@ -198,16 +196,15 @@ impl RbXmlText {
198
196
  let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
199
197
  let iv = ruby_to_invalue(&ruby, value)?;
200
198
  let (doc, id) = (&self.doc, &self.id);
201
- guarded(move || {
202
- let mut txn = doc.transact_mut_with(ORIGIN);
203
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
204
- let at = to_units(&txn, &x, insert_index(index, char_len(&txn, &x)));
199
+ write(doc, move |txn| {
200
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
201
+ let at = to_units(txn, &x, insert_index(index, char_len(txn, &x)));
205
202
  match iv {
206
203
  InValue::Map(entries) => {
207
- x.insert_embed(&mut txn, at, to_map_prelim(entries));
204
+ x.insert_embed(txn, at, to_map_prelim(entries));
208
205
  }
209
206
  InValue::Any(a) => {
210
- x.insert_embed(&mut txn, at, a);
207
+ x.insert_embed(txn, at, a);
211
208
  }
212
209
  }
213
210
  Ok(())
@@ -263,13 +260,12 @@ impl RbXmlText {
263
260
  /// negative index counts from the end, and the length stops at the end.
264
261
  fn delete(&self, index: i64, length: i64) -> Result<(), Error> {
265
262
  let (doc, id) = (&self.doc, &self.id);
266
- guarded(move || {
267
- let mut txn = doc.transact_mut_with(ORIGIN);
268
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
269
- if let Some((from, to)) = delete_span(index, length, char_len(&txn, &x)) {
270
- let (from, to) = (to_units(&txn, &x, from), to_units(&txn, &x, to));
263
+ write(doc, move |txn| {
264
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
265
+ if let Some((from, to)) = delete_span(index, length, char_len(txn, &x)) {
266
+ let (from, to) = (to_units(txn, &x, from), to_units(txn, &x, to));
271
267
  if to > from {
272
- x.remove_range(&mut txn, from, to - from);
268
+ x.remove_range(txn, from, to - from);
273
269
  }
274
270
  }
275
271
  Ok(())
@@ -279,12 +275,11 @@ impl RbXmlText {
279
275
  /// Remove everything: markers, text, and embedded blocks.
280
276
  fn clear(&self) -> Result<(), Error> {
281
277
  let (doc, id) = (&self.doc, &self.id);
282
- guarded(move || {
283
- let mut txn = doc.transact_mut_with(ORIGIN);
284
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
285
- let len = x.len(&txn);
278
+ write(doc, move |txn| {
279
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
280
+ let len = x.len(txn);
286
281
  if len > 0 {
287
- x.remove_range(&mut txn, 0, len);
282
+ x.remove_range(txn, 0, len);
288
283
  }
289
284
  Ok(())
290
285
  })
@@ -300,13 +295,12 @@ impl RbXmlText {
300
295
  let attributes = hash_pairs(&ruby, attributes)?;
301
296
  let content = Piece::read_all(&ruby, content, 0)?;
302
297
  let (doc, id) = (&self.doc, &self.id);
303
- let block = guarded(move || {
304
- let mut txn = doc.transact_mut_with(ORIGIN);
305
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
306
- let count = embedded_xml_texts(&txn, &x).len() as u32;
298
+ let block = write(doc, move |txn| {
299
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
300
+ let count = embedded_xml_texts(txn, &x).len() as u32;
307
301
  let ordinal = insert_index(n, count);
308
- let at = embedded_xml_text_index(&txn, &x, ordinal).unwrap_or_else(|| x.len(&txn));
309
- let child = Piece::Block(attributes, content).write(&mut txn, &x, at);
302
+ let at = embedded_xml_text_index(txn, &x, ordinal).unwrap_or_else(|| x.len(txn));
303
+ let child = Piece::Block(attributes, content).write(txn, &x, at);
310
304
  // The block's identity, not the block: see `Child`.
311
305
  Ok(identity(&child.expect("a block piece writes a block")))
312
306
  })?;
@@ -331,19 +325,18 @@ impl RbXmlText {
331
325
  let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
332
326
  let content = Piece::read_all(&ruby, content, 0)?;
333
327
  let (doc, id) = (&self.doc, &self.id);
334
- guarded(move || {
335
- let mut txn = doc.transact_mut_with(ORIGIN);
336
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
328
+ write(doc, move |txn| {
329
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
337
330
  if clear {
338
- let len = x.len(&txn);
331
+ let len = x.len(txn);
339
332
  if len > 0 {
340
- x.remove_range(&mut txn, 0, len);
333
+ x.remove_range(txn, 0, len);
341
334
  }
342
335
  }
343
- let mut at = x.len(&txn);
336
+ let mut at = x.len(txn);
344
337
  for piece in content {
345
- piece.write(&mut txn, &x, at);
346
- at = x.len(&txn);
338
+ piece.write(txn, &x, at);
339
+ at = x.len(txn);
347
340
  }
348
341
  Ok(())
349
342
  })
@@ -358,15 +351,14 @@ impl RbXmlText {
358
351
  let ruby = Ruby::get().map_err(|e| yrb_error(e.to_string()))?;
359
352
  let marker = hash_pairs(&ruby, marker)?;
360
353
  let (doc, id) = (&rb_self.doc, &rb_self.id);
361
- guarded(move || {
362
- let mut txn = doc.transact_mut_with(ORIGIN);
363
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
364
- let mut at = x.len(&txn);
365
- if at == 0 || ends_with_xml_text(&txn, &x) {
366
- x.insert_embed(&mut txn, at, to_map_prelim(marker));
354
+ write(doc, move |txn| {
355
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
356
+ let mut at = x.len(txn);
357
+ if at == 0 || ends_with_xml_text(txn, &x) {
358
+ x.insert_embed(txn, at, to_map_prelim(marker));
367
359
  at += 1;
368
360
  }
369
- x.insert(&mut txn, at, &text);
361
+ x.insert(txn, at, &text);
370
362
  Ok(())
371
363
  })?;
372
364
  Ok(rb_self)
@@ -376,16 +368,15 @@ impl RbXmlText {
376
368
  /// last). Returns whether there was one.
377
369
  fn delete_xml_text(&self, n: i64) -> Result<bool, Error> {
378
370
  let (doc, id) = (&self.doc, &self.id);
379
- guarded(move || {
380
- let mut txn = doc.transact_mut_with(ORIGIN);
381
- let x = resolve::<XmlTextRef, _>(&txn, id).ok_or("the block no longer exists")?;
382
- let count = embedded_xml_texts(&txn, &x).len() as u32;
371
+ write(doc, move |txn| {
372
+ let x = resolve::<XmlTextRef, _>(txn, id).ok_or("the block no longer exists")?;
373
+ let count = embedded_xml_texts(txn, &x).len() as u32;
383
374
  let Some(at) =
384
- element_index(n, count).and_then(|n| embedded_xml_text_index(&txn, &x, n))
375
+ element_index(n, count).and_then(|n| embedded_xml_text_index(txn, &x, n))
385
376
  else {
386
377
  return Ok(false);
387
378
  };
388
- x.remove_range(&mut txn, at, 1);
379
+ x.remove_range(txn, at, 1);
389
380
  Ok(true)
390
381
  })
391
382
  }
data/lib/y/array.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ # Inside module Y, `Array` is this class.
5
+ class Array
6
+ # Insert a new Y.Text holding `content` at `index` and return a live
7
+ # Y::Text for it. Indexes work as in `insert`: -1 appends.
8
+ def insert_text(index, content = "")
9
+ native_insert_text(index, content)
10
+ end
11
+
12
+ # Append a new Y.Text holding `content` and return a live Y::Text for it.
13
+ def push_text(content = "")
14
+ insert_text(-1, content)
15
+ end
16
+ end
17
+ end
@@ -43,7 +43,7 @@ module Y
43
43
  # Edits the document from Ruby the way a browser does: yields the
44
44
  # current document, records what the block changed, and broadcasts it so
45
45
  # open editors apply it. Returns the update, or nil when the block
46
- # changed nothing.
46
+ # wrote nothing.
47
47
  #
48
48
  # post.collaborative_document(:body).edit do |doc|
49
49
  # Y::Lexxy.append_paragraph(doc, "Reviewed by ops.")
data/lib/y/doc.rb CHANGED
@@ -4,17 +4,43 @@ module Y
4
4
  # Ruby-side conveniences over the native document.
5
5
  class Doc
6
6
  # Runs the block, which edits this document through handles, and returns
7
- # the update its writes produced, or nil when the document didn't change.
8
- # The document has the edit either way. The update is what a process
9
- # saves and broadcasts, one call per chunk as it streams into a document.
7
+ # an update with every local write no edit has returned yet, or nil when
8
+ # there's nothing to send. The document has the writes either way. The
9
+ # update is what a process saves and broadcasts, one call per chunk as it
10
+ # streams into a document.
10
11
  #
11
- # The update holds everything the document gained while the block ran,
12
- # so a write another thread or peer made meanwhile is in it too. Applying
13
- # an update twice changes nothing, so that only adds bytes.
12
+ # The document keeps every local write until an edit returns it, so none
13
+ # is left out: a write made outside an edit, from another thread or
14
+ # fiber, or in a block that raised comes with the next edit's update. An
15
+ # update applied from a peer is never in it, and doesn't wait for the
16
+ # block. Each edit takes everything unsent at once, so the updates edits
17
+ # return have no gaps between them. An edit inside another, including one
18
+ # in a fiber or thread the outer block starts, returns what it took and
19
+ # leaves it for the outer edit to return again. Once the outer edit
20
+ # returns, a thread it started edits on its own.
21
+ #
22
+ # So send every update an edit returns. An edit in a thread the block
23
+ # didn't start, such as a pool thread, isn't nested: it takes the outer
24
+ # block's writes too, and they're sent only if its update is.
25
+ #
26
+ # An exception raised as `edit` returns, such as a Thread#raise or a
27
+ # signal trap, loses the update the edit took. A caller that must send
28
+ # every update masks interrupts around `edit` and doesn't raise from a
29
+ # trap.
14
30
  def edit
15
- before = native_snapshot
16
- yield self
17
- native_update_since(before)
31
+ # The edits running in this fiber, inherited by the fibers and threads
32
+ # their blocks start. Each is `[doc]` until it ends, then empty.
33
+ outer = (Fiber[:yrby_editing] || []).reject(&:empty?)
34
+ nested = outer.any? { |running| running.include?(self) }
35
+ running = [self]
36
+ Fiber[:yrby_editing] = [*outer, running]
37
+ begin
38
+ yield self
39
+ ensure
40
+ running.clear
41
+ Fiber[:yrby_editing] = outer
42
+ end
43
+ take_unsent(nested)
18
44
  end
19
45
 
20
46
  # The ordinal of the top-level block a position falls in, or nil at the
@@ -32,5 +58,22 @@ module Y
32
58
  def find(anchor)
33
59
  native_find(anchor.position, anchor.root)
34
60
  end
61
+
62
+ private
63
+
64
+ # An interrupt that arrives during the take is raised once the masked
65
+ # block ends, and the taken update goes back to the document first, for
66
+ # the next edit to return. A nested edit has already put it back.
67
+ def take_unsent(nested)
68
+ taken = nil
69
+ Thread.handle_interrupt(Object => :never) do
70
+ taken = native_take_unsent
71
+ native_keep_unsent(taken) if taken && nested
72
+ end
73
+ taken
74
+ rescue Exception # rubocop:disable Lint/RescueException -- an interrupt is any exception
75
+ native_keep_unsent(taken) if taken && !nested
76
+ raise
77
+ end
35
78
  end
36
79
  end
data/lib/y/map.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ class Map
5
+ # Store a new Y.Text holding `content` at `key`, replacing what was
6
+ # there, and return a live Y::Text for it. `map[key] = "..."` stores a
7
+ # plain string instead, and the last write to it wins.
8
+ def set_text(key, content = "")
9
+ native_set_text(key, content)
10
+ end
11
+ end
12
+ end
data/lib/y/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Y
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.1"
5
5
  end
data/lib/y.rb CHANGED
@@ -19,6 +19,8 @@ require_relative "y/lexical_markdown"
19
19
  require_relative "y/anchor"
20
20
  require_relative "y/xml_text"
21
21
  require_relative "y/text"
22
+ require_relative "y/map"
23
+ require_relative "y/array"
22
24
  require_relative "y/doc"
23
25
  require_relative "y/tiptap"
24
26
  require_relative "y/decoder"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara
@@ -103,6 +103,7 @@ files:
103
103
  - lib/generators/yrby/tables/templates/create_y_tables.rb
104
104
  - lib/y.rb
105
105
  - lib/y/anchor.rb
106
+ - lib/y/array.rb
106
107
  - lib/y/collaborative.rb
107
108
  - lib/y/collaborative/attribute.rb
108
109
  - lib/y/collaborative/helper.rb
@@ -111,6 +112,7 @@ files:
111
112
  - lib/y/lexical_markdown.rb
112
113
  - lib/y/lexical_writer.rb
113
114
  - lib/y/lexxy.rb
115
+ - lib/y/map.rb
114
116
  - lib/y/rendering.rb
115
117
  - lib/y/text.rb
116
118
  - lib/y/tiptap.rb