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