yrb-lite 0.1.0.beta5 → 0.1.0.beta7
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 +38 -16
- data/README.md +100 -146
- data/ext/yrb_lite/src/lib.rs +67 -326
- data/ext/yrb_lite/src/protocol.rs +330 -0
- data/lib/yrb_lite/version.rb +1 -1
- metadata +2 -1
data/ext/yrb_lite/src/lib.rs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
use magnus::{
|
|
1
|
+
use magnus::{
|
|
2
|
+
function, method, prelude::*, Error, ExceptionClass, RString, Ruby, TryConvert, Value,
|
|
3
|
+
};
|
|
2
4
|
use std::sync::Mutex;
|
|
3
|
-
use yrs::encoding::read::{Cursor, Read};
|
|
4
|
-
use yrs::sync::protocol::MessageReader;
|
|
5
5
|
use yrs::sync::{Awareness, DefaultProtocol, Message, Protocol, SyncMessage};
|
|
6
|
-
use yrs::updates::decoder::
|
|
6
|
+
use yrs::updates::decoder::Decode;
|
|
7
7
|
use yrs::updates::encoder::{Encode, Encoder, EncoderV1};
|
|
8
|
-
use yrs::{
|
|
8
|
+
use yrs::{Doc, ReadTxn, Transact};
|
|
9
|
+
|
|
10
|
+
mod protocol;
|
|
11
|
+
use protocol::{
|
|
12
|
+
classify_message, doc_has_pending, merged_doc_update, update_advances_doc, update_is_ready,
|
|
13
|
+
};
|
|
9
14
|
|
|
10
15
|
/// Wrapper around yrs Doc.
|
|
11
16
|
///
|
|
@@ -123,99 +128,22 @@ fn copy_bytes(s: RString) -> Vec<u8> {
|
|
|
123
128
|
unsafe { s.as_slice() }.to_vec()
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
fn classify_message(bytes: &[u8]) -> u8 {
|
|
137
|
-
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
138
|
-
let msg = match Message::decode(&mut decoder) {
|
|
139
|
-
Ok(msg) => msg,
|
|
140
|
-
Err(_) => return 0, // empty or malformed
|
|
141
|
-
};
|
|
142
|
-
// Any remaining byte means a second message or trailing garbage.
|
|
143
|
-
if decoder.read_u8().is_ok() {
|
|
144
|
-
return 0;
|
|
145
|
-
}
|
|
146
|
-
match msg {
|
|
147
|
-
Message::Sync(SyncMessage::SyncStep1(_)) => 1,
|
|
148
|
-
Message::Sync(SyncMessage::SyncStep2(_)) | Message::Sync(SyncMessage::Update(_)) => 2,
|
|
149
|
-
Message::Awareness(_) => 3,
|
|
150
|
-
Message::AwarenessQuery => 4,
|
|
151
|
-
_ => 0, // Auth / Custom: not part of our model
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/// Merge the document-update deltas (Update / SyncStep2 payloads) carried by a
|
|
156
|
-
/// frame into one update, or `None` if the frame carries no document change
|
|
157
|
-
/// (a request, an awareness update, or a no-op handshake SyncStep2).
|
|
158
|
-
fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
|
|
159
|
-
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
160
|
-
let mut updates: Vec<Vec<u8>> = Vec::new();
|
|
161
|
-
for msg in MessageReader::new(&mut decoder) {
|
|
162
|
-
match msg.map_err(|e| e.to_string())? {
|
|
163
|
-
Message::Sync(SyncMessage::Update(u)) | Message::Sync(SyncMessage::SyncStep2(u)) => {
|
|
164
|
-
updates.push(u)
|
|
165
|
-
}
|
|
166
|
-
_ => {}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
let merged = match updates.len() {
|
|
170
|
-
0 => return Ok(None),
|
|
171
|
-
1 => updates.pop().unwrap(),
|
|
172
|
-
_ => yrs::merge_updates_v1(&updates).map_err(|e| e.to_string())?,
|
|
173
|
-
};
|
|
174
|
-
let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
|
|
175
|
-
// A genuine no-op (e.g. the empty SyncStep2 in an opening handshake) carries
|
|
176
|
-
// no structs, no deletes, and no dependencies. We must NOT treat a causally-
|
|
177
|
-
// pending update as a no-op: since yrs 0.26 such an update reports an empty
|
|
178
|
-
// state_vector (its structs can't integrate yet), but it still carries
|
|
179
|
-
// content and a non-empty lower bound (the deps it's waiting on). Dropping it
|
|
180
|
-
// here would silently swallow a gappy update instead of rejecting + resyncing.
|
|
181
|
-
if update.state_vector().is_empty()
|
|
182
|
-
&& update.delete_set().is_empty()
|
|
183
|
-
&& update.state_vector_lower().is_empty()
|
|
184
|
-
{
|
|
185
|
-
return Ok(None);
|
|
186
|
-
}
|
|
187
|
-
Ok(Some(merged))
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/// Collect the awareness client IDs referenced by a frame's awareness messages.
|
|
191
|
-
fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
|
|
192
|
-
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
193
|
-
let mut ids = Vec::new();
|
|
194
|
-
for msg in MessageReader::new(&mut decoder) {
|
|
195
|
-
if let Message::Awareness(update) = msg.map_err(|e| e.to_string())? {
|
|
196
|
-
ids.extend(update.clients.keys().map(|c| c.get()));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
Ok(ids)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/// True if applying `update_bytes` to `doc` would integrate cleanly: every
|
|
203
|
-
/// dependency the update references is already present (the doc's state vector
|
|
204
|
-
/// covers the update's lower bound). A pure read; does not mutate the doc.
|
|
205
|
-
/// When false, applying it would park a pending struct -- the signal that an
|
|
206
|
-
/// earlier, causally-prior update is missing.
|
|
207
|
-
fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
|
|
208
|
-
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
209
|
-
Ok(doc.transact().state_vector() >= update.state_vector_lower())
|
|
131
|
+
/// Build a `YrbLite::Error` (the gem's own error class, defined in `init`) so
|
|
132
|
+
/// native decode/apply failures surface as a project-specific error rather than
|
|
133
|
+
/// a generic RuntimeError. Falls back to RuntimeError only if the class somehow
|
|
134
|
+
/// can't be resolved.
|
|
135
|
+
fn yrb_error(msg: String) -> Error {
|
|
136
|
+
let ruby = Ruby::get().unwrap();
|
|
137
|
+
let class = ruby
|
|
138
|
+
.eval::<ExceptionClass>("YrbLite::Error")
|
|
139
|
+
.unwrap_or_else(|_| ruby.exception_runtime_error());
|
|
140
|
+
Error::new(class, msg)
|
|
210
141
|
}
|
|
211
142
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let txn = doc.transact();
|
|
217
|
-
txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
|
|
218
|
-
}
|
|
143
|
+
// CLIENT IDs ARE NOT VALIDATED -- whoever supplies the id (the app via
|
|
144
|
+
// `Doc.new(id)` / `Awareness.new(id)`, or a remote peer over the wire) is
|
|
145
|
+
// responsible for keeping it JS-safe (<= 2^53 - 1). See the protocol.rs header
|
|
146
|
+
// for why (and `ClientID::try_new`, proposed upstream, for strict rejection).
|
|
219
147
|
|
|
220
148
|
// ============================================================================
|
|
221
149
|
// Doc Implementation
|
|
@@ -233,17 +161,14 @@ impl RbDoc {
|
|
|
233
161
|
Ok(RbDoc(doc))
|
|
234
162
|
}
|
|
235
163
|
|
|
236
|
-
/// Get the client ID
|
|
237
164
|
fn client_id(&self) -> u64 {
|
|
238
165
|
self.0.client_id().get()
|
|
239
166
|
}
|
|
240
167
|
|
|
241
|
-
/// Get the document GUID
|
|
242
168
|
fn guid(&self) -> String {
|
|
243
169
|
self.0.guid().to_string()
|
|
244
170
|
}
|
|
245
171
|
|
|
246
|
-
/// Get the current state vector encoded as bytes
|
|
247
172
|
fn encode_state_vector(&self) -> RString {
|
|
248
173
|
let doc = &self.0;
|
|
249
174
|
let sv = nogvl(move || {
|
|
@@ -270,11 +195,10 @@ impl RbDoc {
|
|
|
270
195
|
let txn = doc.transact();
|
|
271
196
|
Ok(txn.encode_state_as_update_v1(&sv))
|
|
272
197
|
})
|
|
273
|
-
.map_err(
|
|
198
|
+
.map_err(yrb_error)?;
|
|
274
199
|
Ok(binary_string(&update))
|
|
275
200
|
}
|
|
276
201
|
|
|
277
|
-
/// Apply a V1 update to the document
|
|
278
202
|
fn apply_update(&self, update: RString) -> Result<(), Error> {
|
|
279
203
|
let update_bytes = copy_bytes(update);
|
|
280
204
|
let doc = &self.0;
|
|
@@ -283,7 +207,7 @@ impl RbDoc {
|
|
|
283
207
|
let mut txn = doc.transact_mut();
|
|
284
208
|
txn.apply_update(update).map_err(|e| e.to_string())
|
|
285
209
|
})
|
|
286
|
-
.map_err(
|
|
210
|
+
.map_err(yrb_error)
|
|
287
211
|
}
|
|
288
212
|
|
|
289
213
|
/// True if applying `update` would integrate cleanly (its dependencies are
|
|
@@ -292,7 +216,16 @@ impl RbDoc {
|
|
|
292
216
|
fn update_ready(&self, update: RString) -> Result<bool, Error> {
|
|
293
217
|
let update_bytes = copy_bytes(update);
|
|
294
218
|
let doc = &self.0;
|
|
295
|
-
nogvl(move || update_is_ready(doc, &update_bytes)).map_err(
|
|
219
|
+
nogvl(move || update_is_ready(doc, &update_bytes)).map_err(yrb_error)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// True if applying `update` would change the document (it carries new
|
|
223
|
+
/// content), false if the doc already contains it (an already-applied
|
|
224
|
+
/// retry). See `update_advances_doc`. Pure read; does not mutate.
|
|
225
|
+
fn update_advances(&self, update: RString) -> Result<bool, Error> {
|
|
226
|
+
let update_bytes = copy_bytes(update);
|
|
227
|
+
let doc = &self.0;
|
|
228
|
+
nogvl(move || update_advances_doc(doc, &update_bytes)).map_err(yrb_error)
|
|
296
229
|
}
|
|
297
230
|
|
|
298
231
|
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
@@ -323,7 +256,7 @@ impl RbDoc {
|
|
|
323
256
|
let update = txn.encode_state_as_update_v1(&sv);
|
|
324
257
|
Ok(Message::Sync(SyncMessage::SyncStep2(update)).encode_v1())
|
|
325
258
|
})
|
|
326
|
-
.map_err(
|
|
259
|
+
.map_err(yrb_error)?;
|
|
327
260
|
Ok(binary_string(&encoded))
|
|
328
261
|
}
|
|
329
262
|
|
|
@@ -369,15 +302,15 @@ impl RbDoc {
|
|
|
369
302
|
Message::Custom(tag, _) => Ok((tag, 0, Vec::new())),
|
|
370
303
|
}
|
|
371
304
|
})
|
|
372
|
-
.map_err(
|
|
305
|
+
.map_err(yrb_error)?;
|
|
373
306
|
|
|
374
307
|
Ok(Some((msg_type, sync_type, binary_string(&response))))
|
|
375
308
|
}
|
|
376
309
|
|
|
377
310
|
/// Encode raw update bytes as a sync Update message
|
|
378
311
|
fn encode_update_message(&self, update: RString) -> RString {
|
|
379
|
-
let update_bytes =
|
|
380
|
-
let msg = Message::Sync(SyncMessage::Update(update_bytes
|
|
312
|
+
let update_bytes = copy_bytes(update);
|
|
313
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
381
314
|
binary_string(&msg.encode_v1())
|
|
382
315
|
}
|
|
383
316
|
}
|
|
@@ -398,13 +331,11 @@ impl RbAwareness {
|
|
|
398
331
|
Ok(RbAwareness(Mutex::new(awareness)))
|
|
399
332
|
}
|
|
400
333
|
|
|
401
|
-
/// Get the client ID of the underlying document
|
|
402
334
|
fn client_id(&self) -> u64 {
|
|
403
335
|
let awareness = &self.0;
|
|
404
336
|
nogvl(move || awareness.lock().unwrap().doc().client_id().get())
|
|
405
337
|
}
|
|
406
338
|
|
|
407
|
-
/// Get the document GUID
|
|
408
339
|
fn guid(&self) -> String {
|
|
409
340
|
let awareness = &self.0;
|
|
410
341
|
nogvl(move || awareness.lock().unwrap().doc().guid().to_string())
|
|
@@ -437,7 +368,7 @@ impl RbAwareness {
|
|
|
437
368
|
.map_err(|e| e.to_string())?;
|
|
438
369
|
Ok(encoder.to_vec())
|
|
439
370
|
})
|
|
440
|
-
.map_err(
|
|
371
|
+
.map_err(yrb_error)?;
|
|
441
372
|
Ok(binary_string(&encoded))
|
|
442
373
|
}
|
|
443
374
|
|
|
@@ -464,18 +395,17 @@ impl RbAwareness {
|
|
|
464
395
|
}
|
|
465
396
|
Ok(encoder.to_vec())
|
|
466
397
|
})
|
|
467
|
-
.map_err(
|
|
398
|
+
.map_err(yrb_error)?;
|
|
468
399
|
Ok(binary_string(&encoded))
|
|
469
400
|
}
|
|
470
401
|
|
|
471
402
|
/// Encode an update message for broadcasting changes to peers.
|
|
472
403
|
fn encode_update(&self, update: RString) -> RString {
|
|
473
|
-
let update_bytes =
|
|
474
|
-
let msg = Message::Sync(SyncMessage::Update(update_bytes
|
|
404
|
+
let update_bytes = copy_bytes(update);
|
|
405
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
475
406
|
binary_string(&msg.encode_v1())
|
|
476
407
|
}
|
|
477
408
|
|
|
478
|
-
/// Get the current state vector encoded as bytes
|
|
479
409
|
fn encode_state_vector(&self) -> RString {
|
|
480
410
|
let awareness = &self.0;
|
|
481
411
|
let sv = nogvl(move || {
|
|
@@ -504,14 +434,14 @@ impl RbAwareness {
|
|
|
504
434
|
let txn = doc.transact();
|
|
505
435
|
Ok(txn.encode_state_as_update_v1(&sv))
|
|
506
436
|
})
|
|
507
|
-
.map_err(
|
|
437
|
+
.map_err(yrb_error)?;
|
|
508
438
|
Ok(binary_string(&update))
|
|
509
439
|
}
|
|
510
440
|
|
|
511
441
|
/// Set local awareness state (JSON string)
|
|
512
442
|
fn set_local_state(&self, json: String) -> Result<(), Error> {
|
|
513
443
|
let value: serde_json::Value =
|
|
514
|
-
serde_json::from_str(&json).map_err(|e|
|
|
444
|
+
serde_json::from_str(&json).map_err(|e| yrb_error(e.to_string()))?;
|
|
515
445
|
let awareness = &self.0;
|
|
516
446
|
nogvl(move || -> Result<(), String> {
|
|
517
447
|
awareness
|
|
@@ -520,7 +450,7 @@ impl RbAwareness {
|
|
|
520
450
|
.set_local_state(value)
|
|
521
451
|
.map_err(|e| e.to_string())
|
|
522
452
|
})
|
|
523
|
-
.map_err(
|
|
453
|
+
.map_err(yrb_error)
|
|
524
454
|
}
|
|
525
455
|
|
|
526
456
|
/// Get local awareness state as JSON string (or nil if not set)
|
|
@@ -549,11 +479,10 @@ impl RbAwareness {
|
|
|
549
479
|
let update = awareness.update().map_err(|e| e.to_string())?;
|
|
550
480
|
Ok(Message::Awareness(update).encode_v1())
|
|
551
481
|
})
|
|
552
|
-
.map_err(
|
|
482
|
+
.map_err(yrb_error)?;
|
|
553
483
|
Ok(binary_string(&encoded))
|
|
554
484
|
}
|
|
555
485
|
|
|
556
|
-
/// Apply a raw update to the underlying document
|
|
557
486
|
fn apply_update(&self, update: RString) -> Result<(), Error> {
|
|
558
487
|
let update_bytes = copy_bytes(update);
|
|
559
488
|
let awareness = &self.0;
|
|
@@ -563,7 +492,7 @@ impl RbAwareness {
|
|
|
563
492
|
let mut txn = doc.transact_mut();
|
|
564
493
|
txn.apply_update(update).map_err(|e| e.to_string())
|
|
565
494
|
})
|
|
566
|
-
.map_err(
|
|
495
|
+
.map_err(yrb_error)
|
|
567
496
|
}
|
|
568
497
|
|
|
569
498
|
/// True if applying `update` would integrate cleanly (its dependencies are
|
|
@@ -576,7 +505,19 @@ impl RbAwareness {
|
|
|
576
505
|
let doc = awareness.lock().unwrap().doc().clone();
|
|
577
506
|
update_is_ready(&doc, &update_bytes)
|
|
578
507
|
})
|
|
579
|
-
.map_err(
|
|
508
|
+
.map_err(yrb_error)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/// True if applying `update` would change the document, false if it's an
|
|
512
|
+
/// already-applied retry. See `update_advances_doc`. Pure read.
|
|
513
|
+
fn update_advances(&self, update: RString) -> Result<bool, Error> {
|
|
514
|
+
let update_bytes = copy_bytes(update);
|
|
515
|
+
let awareness = &self.0;
|
|
516
|
+
nogvl(move || {
|
|
517
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
518
|
+
update_advances_doc(&doc, &update_bytes)
|
|
519
|
+
})
|
|
520
|
+
.map_err(yrb_error)
|
|
580
521
|
}
|
|
581
522
|
|
|
582
523
|
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
@@ -589,16 +530,6 @@ impl RbAwareness {
|
|
|
589
530
|
})
|
|
590
531
|
}
|
|
591
532
|
|
|
592
|
-
/// Decode the awareness client IDs referenced by a protocol message
|
|
593
|
-
/// (which may pack several sub-messages together). Sync sub-messages are
|
|
594
|
-
/// ignored. The ActionCable layer uses this to learn which presence
|
|
595
|
-
/// states arrived on a connection, so it can clear them when that
|
|
596
|
-
/// connection closes.
|
|
597
|
-
fn awareness_client_ids(&self, data: RString) -> Result<Vec<u64>, Error> {
|
|
598
|
-
let data_bytes = copy_bytes(data);
|
|
599
|
-
nogvl(move || awareness_client_ids_in(&data_bytes)).map_err(runtime_error)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
533
|
/// Classify a frame for safe routing and relay. Returns a code only when
|
|
603
534
|
/// the frame is exactly one well-formed message that consumes the whole
|
|
604
535
|
/// buffer, so a malformed, truncated, multi-message, or trailing-garbage
|
|
@@ -617,42 +548,13 @@ impl RbAwareness {
|
|
|
617
548
|
/// Extract the document-update delta carried by a protocol message: the
|
|
618
549
|
/// payloads of any Update or SyncStep2 sub-messages, merged into a single
|
|
619
550
|
/// update. Returns nil if the message carries no document change (for
|
|
620
|
-
/// instance a SyncStep1 request or an awareness update). The
|
|
621
|
-
/// path records this exact delta before
|
|
551
|
+
/// instance a SyncStep1 request or an awareness update). The store-backed
|
|
552
|
+
/// path records this exact delta before relaying it.
|
|
622
553
|
fn update_from_message(&self, data: RString) -> Result<Option<RString>, Error> {
|
|
623
554
|
let data_bytes = copy_bytes(data);
|
|
624
|
-
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(
|
|
555
|
+
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(yrb_error)?;
|
|
625
556
|
Ok(merged.map(|b| binary_string(&b)))
|
|
626
557
|
}
|
|
627
|
-
|
|
628
|
-
/// Mark the given clients as disconnected and return an awareness protocol
|
|
629
|
-
/// message (null-state, bumped clock) announcing their removal to peers.
|
|
630
|
-
/// Only clients currently known to this Awareness are removed; unknown
|
|
631
|
-
/// IDs are skipped (so we never broadcast phantom removals). Returns an
|
|
632
|
-
/// empty string when nothing was removed.
|
|
633
|
-
fn remove_clients(&self, client_ids: Vec<u64>) -> Result<RString, Error> {
|
|
634
|
-
let awareness = &self.0;
|
|
635
|
-
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
636
|
-
let mut awareness = awareness.lock().unwrap();
|
|
637
|
-
let mut removed = Vec::new();
|
|
638
|
-
for id in client_ids {
|
|
639
|
-
let cid = ClientID::new(id);
|
|
640
|
-
if awareness.meta(cid).is_some() {
|
|
641
|
-
awareness.remove_state(cid);
|
|
642
|
-
removed.push(cid);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if removed.is_empty() {
|
|
646
|
-
return Ok(Vec::new());
|
|
647
|
-
}
|
|
648
|
-
let update = awareness
|
|
649
|
-
.update_with_clients(removed)
|
|
650
|
-
.map_err(|e| e.to_string())?;
|
|
651
|
-
Ok(Message::Awareness(update).encode_v1())
|
|
652
|
-
})
|
|
653
|
-
.map_err(runtime_error)?;
|
|
654
|
-
Ok(binary_string(&encoded))
|
|
655
|
-
}
|
|
656
558
|
}
|
|
657
559
|
|
|
658
560
|
// ============================================================================
|
|
@@ -682,6 +584,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
682
584
|
)?;
|
|
683
585
|
doc_class.define_method("apply_update", method!(RbDoc::apply_update, 1))?;
|
|
684
586
|
doc_class.define_method("update_ready?", method!(RbDoc::update_ready, 1))?;
|
|
587
|
+
doc_class.define_method("update_advances?", method!(RbDoc::update_advances, 1))?;
|
|
685
588
|
doc_class.define_method("pending?", method!(RbDoc::pending, 0))?;
|
|
686
589
|
doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
|
|
687
590
|
doc_class.define_method("sync_step2", method!(RbDoc::sync_step2, 1))?;
|
|
@@ -713,6 +616,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
713
616
|
)?;
|
|
714
617
|
awareness_class.define_method("apply_update", method!(RbAwareness::apply_update, 1))?;
|
|
715
618
|
awareness_class.define_method("update_ready?", method!(RbAwareness::update_ready, 1))?;
|
|
619
|
+
awareness_class.define_method("update_advances?", method!(RbAwareness::update_advances, 1))?;
|
|
716
620
|
awareness_class.define_method("pending?", method!(RbAwareness::pending, 0))?;
|
|
717
621
|
awareness_class.define_method("set_local_state", method!(RbAwareness::set_local_state, 1))?;
|
|
718
622
|
awareness_class.define_method("local_state", method!(RbAwareness::local_state, 0))?;
|
|
@@ -724,11 +628,6 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
724
628
|
"encode_awareness_update",
|
|
725
629
|
method!(RbAwareness::encode_awareness_update, 0),
|
|
726
630
|
)?;
|
|
727
|
-
awareness_class.define_method(
|
|
728
|
-
"awareness_client_ids",
|
|
729
|
-
method!(RbAwareness::awareness_client_ids, 1),
|
|
730
|
-
)?;
|
|
731
|
-
awareness_class.define_method("remove_clients", method!(RbAwareness::remove_clients, 1))?;
|
|
732
631
|
awareness_class.define_method(
|
|
733
632
|
"update_from_message",
|
|
734
633
|
method!(RbAwareness::update_from_message, 1),
|
|
@@ -746,161 +645,3 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
746
645
|
|
|
747
646
|
Ok(())
|
|
748
647
|
}
|
|
749
|
-
|
|
750
|
-
// ============================================================================
|
|
751
|
-
// Tests for the pure protocol helpers (run with `cargo test`, no Ruby VM)
|
|
752
|
-
// ============================================================================
|
|
753
|
-
|
|
754
|
-
#[cfg(test)]
|
|
755
|
-
mod tests {
|
|
756
|
-
use super::*;
|
|
757
|
-
use yrs::sync::Awareness;
|
|
758
|
-
use yrs::Text;
|
|
759
|
-
|
|
760
|
-
fn text_update(content: &str) -> Vec<u8> {
|
|
761
|
-
let doc = Doc::new();
|
|
762
|
-
let text = doc.get_or_insert_text("content");
|
|
763
|
-
text.insert(&mut doc.transact_mut(), 0, content);
|
|
764
|
-
let update = doc
|
|
765
|
-
.transact()
|
|
766
|
-
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
767
|
-
update
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
fn update_frame(content: &str) -> Vec<u8> {
|
|
771
|
-
Message::Sync(SyncMessage::Update(text_update(content))).encode_v1()
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
fn step1_frame() -> Vec<u8> {
|
|
775
|
-
Message::Sync(SyncMessage::SyncStep1(yrs::StateVector::default())).encode_v1()
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
fn awareness_frame(client_id: u64) -> Vec<u8> {
|
|
779
|
-
let mut awareness = Awareness::new(Doc::with_client_id(client_id));
|
|
780
|
-
awareness
|
|
781
|
-
.set_local_state(serde_json::json!({ "user": "alice" }))
|
|
782
|
-
.unwrap();
|
|
783
|
-
Message::Awareness(awareness.update().unwrap()).encode_v1()
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
#[test]
|
|
787
|
-
fn classify_accepts_clean_single_messages() {
|
|
788
|
-
assert_eq!(classify_message(&step1_frame()), 1);
|
|
789
|
-
assert_eq!(classify_message(&update_frame("hi")), 2);
|
|
790
|
-
assert_eq!(classify_message(&awareness_frame(7)), 3);
|
|
791
|
-
assert_eq!(classify_message(&Message::AwarenessQuery.encode_v1()), 4);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
#[test]
|
|
795
|
-
fn classify_rejects_unsafe_frames() {
|
|
796
|
-
assert_eq!(classify_message(b""), 0, "empty");
|
|
797
|
-
assert_eq!(classify_message(&[0xff, 0xff, 0xff]), 0, "garbage");
|
|
798
|
-
assert_eq!(classify_message(&[0x63, 0x63, 0x63]), 0, "unknown type");
|
|
799
|
-
|
|
800
|
-
let mut two = update_frame("a");
|
|
801
|
-
two.extend(awareness_frame(1)); // two messages packed together
|
|
802
|
-
assert_eq!(classify_message(&two), 0, "multi-message");
|
|
803
|
-
|
|
804
|
-
let mut trailing = update_frame("a");
|
|
805
|
-
trailing.extend_from_slice(&[0xde, 0xad]);
|
|
806
|
-
assert_eq!(classify_message(&trailing), 0, "trailing garbage");
|
|
807
|
-
|
|
808
|
-
let frame = update_frame("hello");
|
|
809
|
-
assert_eq!(classify_message(&frame[..frame.len() / 2]), 0, "truncated");
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
#[test]
|
|
813
|
-
fn merged_doc_update_extracts_and_skips_no_ops() {
|
|
814
|
-
// A document update yields a delta that reconstructs the content.
|
|
815
|
-
let delta = merged_doc_update(&update_frame("hello"))
|
|
816
|
-
.unwrap()
|
|
817
|
-
.expect("a document update");
|
|
818
|
-
let doc = Doc::new();
|
|
819
|
-
doc.transact_mut()
|
|
820
|
-
.apply_update(yrs::Update::decode_v1(&delta).unwrap())
|
|
821
|
-
.unwrap();
|
|
822
|
-
// The delta carried real content, so applying it advances the doc.
|
|
823
|
-
assert!(!doc.transact().state_vector().is_empty());
|
|
824
|
-
|
|
825
|
-
// A SyncStep1 request carries no document change.
|
|
826
|
-
assert!(merged_doc_update(&step1_frame()).unwrap().is_none());
|
|
827
|
-
|
|
828
|
-
// An empty SyncStep2 (no new structs) is a no-op.
|
|
829
|
-
let empty = Message::Sync(SyncMessage::SyncStep2(
|
|
830
|
-
Doc::new()
|
|
831
|
-
.transact()
|
|
832
|
-
.encode_state_as_update_v1(&yrs::StateVector::default()),
|
|
833
|
-
))
|
|
834
|
-
.encode_v1();
|
|
835
|
-
assert!(merged_doc_update(&empty).unwrap().is_none());
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
#[test]
|
|
839
|
-
fn merged_doc_update_merges_multiple_updates() {
|
|
840
|
-
// Two updates from different clients packed in one frame merge into one.
|
|
841
|
-
let mut frame = update_frame("a");
|
|
842
|
-
frame.extend(update_frame("b"));
|
|
843
|
-
let merged = merged_doc_update(&frame).unwrap().expect("merged update");
|
|
844
|
-
|
|
845
|
-
// The merged update must decode cleanly as a single update.
|
|
846
|
-
assert!(yrs::Update::decode_v1(&merged).is_ok());
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
#[test]
|
|
850
|
-
fn awareness_client_ids_are_collected() {
|
|
851
|
-
assert_eq!(
|
|
852
|
-
awareness_client_ids_in(&awareness_frame(111)).unwrap(),
|
|
853
|
-
vec![111]
|
|
854
|
-
);
|
|
855
|
-
// A document frame has no awareness client ids.
|
|
856
|
-
assert!(awareness_client_ids_in(&update_frame("x"))
|
|
857
|
-
.unwrap()
|
|
858
|
-
.is_empty());
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
#[test]
|
|
862
|
-
fn update_readiness_and_pending_detect_a_causal_gap() {
|
|
863
|
-
// Three sequential single-char inserts from one client: A, then B, then
|
|
864
|
-
// C. Each delta depends on the previous, so C can't integrate without B.
|
|
865
|
-
let src = Doc::new();
|
|
866
|
-
let txt = src.get_or_insert_text("t");
|
|
867
|
-
let mut deltas: Vec<Vec<u8>> = Vec::new();
|
|
868
|
-
let mut prev = yrs::StateVector::default();
|
|
869
|
-
for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
|
|
870
|
-
txt.insert(&mut src.transact_mut(), i as u32, ch);
|
|
871
|
-
deltas.push(src.transact().encode_state_as_update_v1(&prev));
|
|
872
|
-
prev = src.transact().state_vector();
|
|
873
|
-
}
|
|
874
|
-
let (u1, u2, u3) = (&deltas[0], &deltas[1], &deltas[2]);
|
|
875
|
-
|
|
876
|
-
// A doc holding only u1 (u2 was lost in transit / its record failed):
|
|
877
|
-
let doc = Doc::new();
|
|
878
|
-
doc.transact_mut()
|
|
879
|
-
.apply_update(yrs::Update::decode_v1(u1).unwrap())
|
|
880
|
-
.unwrap();
|
|
881
|
-
assert!(update_is_ready(&doc, u1).unwrap(), "u1 has no missing deps");
|
|
882
|
-
assert!(
|
|
883
|
-
!update_is_ready(&doc, u3).unwrap(),
|
|
884
|
-
"u3 depends on the missing u2"
|
|
885
|
-
);
|
|
886
|
-
assert!(
|
|
887
|
-
!doc_has_pending(&doc),
|
|
888
|
-
"nothing pending until u3 is applied"
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
// Applying u3 anyway parks it as a pending struct.
|
|
892
|
-
doc.transact_mut()
|
|
893
|
-
.apply_update(yrs::Update::decode_v1(u3).unwrap())
|
|
894
|
-
.unwrap();
|
|
895
|
-
assert!(
|
|
896
|
-
doc_has_pending(&doc),
|
|
897
|
-
"u3 is pending: its parent u2 is missing"
|
|
898
|
-
);
|
|
899
|
-
|
|
900
|
-
// Once u2 arrives (via resync), u3 integrates and pending clears.
|
|
901
|
-
doc.transact_mut()
|
|
902
|
-
.apply_update(yrs::Update::decode_v1(u2).unwrap())
|
|
903
|
-
.unwrap();
|
|
904
|
-
assert!(!doc_has_pending(&doc), "u2 arrived; u3 integrated");
|
|
905
|
-
}
|
|
906
|
-
}
|