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 +4 -4
- data/CHANGELOG.md +27 -0
- data/Cargo.lock +12 -20
- data/ext/yrby/Cargo.toml +1 -1
- data/ext/yrby/src/array.rs +37 -25
- data/ext/yrby/src/lib.rs +146 -99
- data/ext/yrby/src/map.rs +26 -15
- data/ext/yrby/src/shared.rs +29 -10
- data/ext/yrby/src/text.rs +14 -17
- data/ext/yrby/src/xml_text.rs +46 -55
- data/lib/y/array.rb +17 -0
- data/lib/y/collaborative/attribute.rb +1 -1
- data/lib/y/doc.rb +52 -9
- data/lib/y/map.rb +12 -0
- data/lib/y/version.rb +1 -1
- data/lib/y.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b872650c1daa2696dc90e8f209b3dba702d7bb796f5af330550343a7e7397e51
|
|
4
|
+
data.tar.gz: 9070c8d98f2323f80ea60e79d59c307a47486465093c384182252136faab69d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
47
|
+
version = "0.72.1"
|
|
48
48
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
49
|
-
checksum = "
|
|
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.
|
|
361
|
+
version = "0.9.130"
|
|
370
362
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
371
|
-
checksum = "
|
|
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.
|
|
370
|
+
version = "0.9.130"
|
|
379
371
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
380
|
-
checksum = "
|
|
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 = "
|
|
456
|
+
version = "2.1.3"
|
|
465
457
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
466
|
-
checksum = "
|
|
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.
|
|
544
|
+
version = "1.16.4"
|
|
553
545
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
554
|
-
checksum = "
|
|
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.
|
|
667
|
+
version = "0.28.1"
|
|
676
668
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
677
|
-
checksum = "
|
|
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" }
|
data/ext/yrby/src/array.rs
CHANGED
|
@@ -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,
|
|
24
|
-
Child,
|
|
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
|
-
|
|
166
|
-
let
|
|
167
|
-
let
|
|
168
|
-
|
|
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> =
|
|
178
|
-
let
|
|
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(
|
|
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(
|
|
187
|
-
a.remove(
|
|
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
|
-
|
|
200
|
-
let
|
|
201
|
-
|
|
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(
|
|
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
|
-
|
|
219
|
-
let
|
|
220
|
-
let
|
|
221
|
-
|
|
222
|
-
a.
|
|
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
|
-
|
|
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::
|
|
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
|
-
///
|
|
402
|
-
///
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
///
|
|
410
|
-
///
|
|
411
|
-
fn
|
|
412
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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("
|
|
1186
|
-
doc_class.define_private_method("
|
|
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,
|
|
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
|
-
|
|
172
|
-
let
|
|
173
|
-
|
|
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> =
|
|
185
|
-
let
|
|
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(
|
|
191
|
-
m.remove(
|
|
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
|
-
|
|
204
|
-
let
|
|
205
|
-
|
|
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))?;
|
data/ext/yrby/src/shared.rs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
///
|
|
41
|
-
///
|
|
42
|
-
/// ones.
|
|
43
|
-
pub fn
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
88
|
-
let
|
|
89
|
-
let
|
|
90
|
-
|
|
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
|
-
|
|
141
|
-
let
|
|
142
|
-
let
|
|
143
|
-
|
|
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(
|
|
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
|
-
|
|
156
|
-
let
|
|
157
|
-
|
|
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(
|
|
157
|
+
t.remove_range(txn, 0, len);
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
Ok(())
|
data/ext/yrby/src/xml_text.rs
CHANGED
|
@@ -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,
|
|
27
|
-
|
|
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
|
-
|
|
170
|
-
let
|
|
171
|
-
|
|
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
|
-
|
|
184
|
-
let
|
|
185
|
-
let
|
|
186
|
-
|
|
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
|
-
|
|
202
|
-
let
|
|
203
|
-
let
|
|
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(
|
|
204
|
+
x.insert_embed(txn, at, to_map_prelim(entries));
|
|
208
205
|
}
|
|
209
206
|
InValue::Any(a) => {
|
|
210
|
-
x.insert_embed(
|
|
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
|
-
|
|
267
|
-
let
|
|
268
|
-
let
|
|
269
|
-
|
|
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(
|
|
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
|
-
|
|
283
|
-
let
|
|
284
|
-
let
|
|
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(
|
|
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 =
|
|
304
|
-
let
|
|
305
|
-
let
|
|
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(
|
|
309
|
-
let child = Piece::Block(attributes, content).write(
|
|
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
|
-
|
|
335
|
-
let
|
|
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(
|
|
331
|
+
let len = x.len(txn);
|
|
339
332
|
if len > 0 {
|
|
340
|
-
x.remove_range(
|
|
333
|
+
x.remove_range(txn, 0, len);
|
|
341
334
|
}
|
|
342
335
|
}
|
|
343
|
-
let mut at = x.len(
|
|
336
|
+
let mut at = x.len(txn);
|
|
344
337
|
for piece in content {
|
|
345
|
-
piece.write(
|
|
346
|
-
at = x.len(
|
|
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
|
-
|
|
362
|
-
let
|
|
363
|
-
let
|
|
364
|
-
|
|
365
|
-
|
|
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(
|
|
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
|
-
|
|
380
|
-
let
|
|
381
|
-
let
|
|
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(
|
|
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(
|
|
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
|
-
#
|
|
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
|
-
#
|
|
8
|
-
# The document has the
|
|
9
|
-
# saves and broadcasts, one call per chunk as it
|
|
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
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
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.
|
|
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
|