yrb-lite 0.1.0.beta2 → 0.1.0.beta3
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 +24 -1
- data/ext/yrb_lite/Cargo.toml +1 -1
- data/ext/yrb_lite/src/lib.rs +102 -47
- data/lib/yrb_lite/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdc68c7b4936a2e6598441f2def20050e456a543f44de6ecd35aeb1a894cdcc7
|
|
4
|
+
data.tar.gz: 5a5ffbf2c1df1fc41d7eba424579459587d46e1e38db0606b79efefb5d5321c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00bff87ec9b51a46f7bee55e838ef629531c0f4faaa43400f2447055932fefc4d8ce597c15724b98e384bbd5f119c881ebf1f85f2b2bab614ff6d3e00d570eaa
|
|
7
|
+
data.tar.gz: 0dff053de27327d7517bdb83b850ee45c977d38f1556ee38210e8e3431da19937c1cc4e22201da2242ca249cee1a1647b8f0ec5dd1fe50ed75fb2d78743a4947
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.0.beta3] - 2026-06-18
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Upgraded the bundled `yrs` (y-crdt) from 0.21 to 0.27.2. No change to the
|
|
14
|
+
`YrbLite::Doc`, `YrbLite::Awareness`, or `YrbLite::Sync` public API; existing
|
|
15
|
+
code and the wire protocol are unaffected.
|
|
16
|
+
- Thread-safety is preserved across the upgrade. yrs 0.27 dropped `Awareness`'s
|
|
17
|
+
internal locking (its mutating methods now take `&mut self`, and `Awareness`
|
|
18
|
+
is no longer `Sync`), so `YrbLite::Awareness` now serializes access through an
|
|
19
|
+
internal `Mutex`. The lock is taken only while the GVL is released and is
|
|
20
|
+
never held across the GVL boundary, so concurrent access from multiple Ruby
|
|
21
|
+
threads stays safe and deadlock-free, and document reads still run in parallel
|
|
22
|
+
(they operate on a cheaply-cloned, `Arc`-backed `Doc` handle, not under the
|
|
23
|
+
presence lock).
|
|
24
|
+
|
|
25
|
+
### Build
|
|
26
|
+
|
|
27
|
+
- Building the gem from source now requires **Rust 1.94 or newer** (yrs 0.27.2
|
|
28
|
+
uses `let`-chains). The precompiled platform gems are unaffected -- they need
|
|
29
|
+
no Rust toolchain to install.
|
|
30
|
+
|
|
9
31
|
## [0.1.0.beta2] - 2026-06-16
|
|
10
32
|
|
|
11
33
|
### Added
|
|
@@ -54,6 +76,7 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
54
76
|
- Precompiled native gems for common platforms (no Rust toolchain needed to
|
|
55
77
|
install) via the cross-gem workflow.
|
|
56
78
|
|
|
57
|
-
[Unreleased]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.
|
|
79
|
+
[Unreleased]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta3...main
|
|
80
|
+
[0.1.0.beta3]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta2...v0.1.0.beta3
|
|
58
81
|
[0.1.0.beta2]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta1...v0.1.0.beta2
|
|
59
82
|
[0.1.0.beta1]: https://github.com/jpcamara/yrb-lite/releases/tag/v0.1.0.beta1
|
data/ext/yrb_lite/Cargo.toml
CHANGED
data/ext/yrb_lite/src/lib.rs
CHANGED
|
@@ -4,7 +4,8 @@ use yrs::sync::protocol::MessageReader;
|
|
|
4
4
|
use yrs::sync::{Awareness, DefaultProtocol, Message, Protocol, SyncMessage};
|
|
5
5
|
use yrs::updates::decoder::{Decode, DecoderV1};
|
|
6
6
|
use yrs::updates::encoder::{Encode, Encoder, EncoderV1};
|
|
7
|
-
use
|
|
7
|
+
use std::sync::Mutex;
|
|
8
|
+
use yrs::{ClientID, Doc, ReadTxn, Transact};
|
|
8
9
|
|
|
9
10
|
/// Wrapper around yrs Doc.
|
|
10
11
|
///
|
|
@@ -18,20 +19,35 @@ struct RbDoc(Doc);
|
|
|
18
19
|
|
|
19
20
|
/// Wrapper around yrs Awareness (which contains a Doc).
|
|
20
21
|
///
|
|
21
|
-
/// Thread safety: `
|
|
22
|
-
///
|
|
23
|
-
///
|
|
22
|
+
/// Thread safety: as of yrs 0.27 `Awareness` dropped its internal locking and
|
|
23
|
+
/// its mutating methods (`handle`, `set_local_state`, `clean_local_state`,
|
|
24
|
+
/// `remove_state`, `update_with_clients`) take `&mut self`. It is `Send` but no
|
|
25
|
+
/// longer `Sync`, so we serialize all access through a `Mutex`.
|
|
26
|
+
///
|
|
27
|
+
/// CRITICAL: the `Mutex` is ALWAYS locked inside the `nogvl` closure (never with
|
|
28
|
+
/// the GVL held) and the guard is dropped before the closure returns. This obeys
|
|
29
|
+
/// the same rule as the doc's RwLock (see `nogvl`): a thread never waits on this
|
|
30
|
+
/// lock while holding the GVL, and never reacquires the GVL while holding this
|
|
31
|
+
/// lock, so the GVL and this `Mutex` can't deadlock on lock order. Locking with
|
|
32
|
+
/// the GVL held (outside `nogvl`) reintroduces that deadlock -- don't.
|
|
33
|
+
///
|
|
34
|
+
/// For doc-only reads we clone the (Arc-backed) `Doc` out under the brief lock
|
|
35
|
+
/// and operate on the owned clone, so a long encode holds only the doc's own
|
|
36
|
+
/// RwLock, not this `Mutex`, and never blocks presence updates on another
|
|
37
|
+
/// thread. Lock order is always Mutex-then-doc-RwLock (or doc-RwLock alone),
|
|
38
|
+
/// never the reverse.
|
|
24
39
|
#[magnus::wrap(class = "YrbLite::Awareness", free_immediately, size)]
|
|
25
|
-
struct RbAwareness(Awareness);
|
|
40
|
+
struct RbAwareness(Mutex<Awareness>);
|
|
26
41
|
|
|
27
42
|
/// Compile-time proof that the wrapped types are thread-safe. If a future
|
|
28
|
-
/// yrs upgrade makes Doc or Awareness lose Send
|
|
29
|
-
/// instead of silently shipping a thread-unsafe gem.
|
|
43
|
+
/// yrs upgrade makes Doc lose Send/Sync, or Awareness lose Send, this fails the
|
|
44
|
+
/// build instead of silently shipping a thread-unsafe gem. (Awareness is no
|
|
45
|
+
/// longer `Sync` as of yrs 0.27, hence the `Mutex` wrapper, which restores it.)
|
|
30
46
|
#[allow(dead_code)]
|
|
31
47
|
fn assert_thread_safe() {
|
|
32
48
|
fn is_send_sync<T: Send + Sync>() {}
|
|
33
49
|
is_send_sync::<Doc>();
|
|
34
|
-
is_send_sync::<Awareness
|
|
50
|
+
is_send_sync::<Mutex<Awareness>>();
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
/// Run `f` with the GVL (Global VM Lock) released, so other Ruby threads,
|
|
@@ -42,10 +58,13 @@ fn assert_thread_safe() {
|
|
|
42
58
|
/// out of Ruby strings before entering, and results are converted to Ruby
|
|
43
59
|
/// objects after returning.
|
|
44
60
|
/// - It must be `Send` (it runs while other threads own the GVL). `&Doc` and
|
|
45
|
-
/// `&Awareness
|
|
46
|
-
/// -
|
|
47
|
-
///
|
|
48
|
-
/// lock
|
|
61
|
+
/// `&Mutex<Awareness>` are fine: both are `Sync` (asserted above).
|
|
62
|
+
/// - LOCK DISCIPLINE: any native lock it takes -- the doc's internal RwLock OR
|
|
63
|
+
/// the awareness `Mutex` (`self.0.lock()`) -- must be acquired AND released
|
|
64
|
+
/// inside this closure (GVL already dropped). Never lock with the GVL held
|
|
65
|
+
/// (e.g. before calling `nogvl`), or a thread waiting on the lock while
|
|
66
|
+
/// holding the GVL can deadlock against the GVL reacquire. Same reason we
|
|
67
|
+
/// never hold a lock across the GVL boundary.
|
|
49
68
|
///
|
|
50
69
|
/// Panics inside the closure are caught and re-raised (resumed) after the GVL
|
|
51
70
|
/// is reacquired, where magnus converts them to Ruby exceptions.
|
|
@@ -153,8 +172,17 @@ fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
|
|
|
153
172
|
_ => yrs::merge_updates_v1(&updates).map_err(|e| e.to_string())?,
|
|
154
173
|
};
|
|
155
174
|
let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
|
|
156
|
-
|
|
157
|
-
|
|
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);
|
|
158
186
|
}
|
|
159
187
|
Ok(Some(merged))
|
|
160
188
|
}
|
|
@@ -165,7 +193,7 @@ fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
|
|
|
165
193
|
let mut ids = Vec::new();
|
|
166
194
|
for msg in MessageReader::new(&mut decoder) {
|
|
167
195
|
if let Message::Awareness(update) = msg.map_err(|e| e.to_string())? {
|
|
168
|
-
ids.extend(update.clients.keys().
|
|
196
|
+
ids.extend(update.clients.keys().map(|c| c.get()));
|
|
169
197
|
}
|
|
170
198
|
}
|
|
171
199
|
Ok(ids)
|
|
@@ -207,7 +235,7 @@ impl RbDoc {
|
|
|
207
235
|
|
|
208
236
|
/// Get the client ID
|
|
209
237
|
fn client_id(&self) -> u64 {
|
|
210
|
-
self.0.client_id()
|
|
238
|
+
self.0.client_id().get()
|
|
211
239
|
}
|
|
212
240
|
|
|
213
241
|
/// Get the document GUID
|
|
@@ -367,17 +395,19 @@ impl RbAwareness {
|
|
|
367
395
|
let client_id: u64 = TryConvert::try_convert(args[0])?;
|
|
368
396
|
Awareness::new(Doc::with_client_id(client_id))
|
|
369
397
|
};
|
|
370
|
-
Ok(RbAwareness(awareness))
|
|
398
|
+
Ok(RbAwareness(Mutex::new(awareness)))
|
|
371
399
|
}
|
|
372
400
|
|
|
373
401
|
/// Get the client ID of the underlying document
|
|
374
402
|
fn client_id(&self) -> u64 {
|
|
375
|
-
self.0
|
|
403
|
+
let awareness = &self.0;
|
|
404
|
+
nogvl(move || awareness.lock().unwrap().doc().client_id().get())
|
|
376
405
|
}
|
|
377
406
|
|
|
378
407
|
/// Get the document GUID
|
|
379
408
|
fn guid(&self) -> String {
|
|
380
|
-
self.0
|
|
409
|
+
let awareness = &self.0;
|
|
410
|
+
nogvl(move || awareness.lock().unwrap().doc().guid().to_string())
|
|
381
411
|
}
|
|
382
412
|
|
|
383
413
|
/// A standalone SyncStep1 message (the server's state vector). Sent as its
|
|
@@ -386,7 +416,8 @@ impl RbAwareness {
|
|
|
386
416
|
fn sync_step1(&self) -> RString {
|
|
387
417
|
let awareness = &self.0;
|
|
388
418
|
let encoded = nogvl(move || {
|
|
389
|
-
let
|
|
419
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
420
|
+
let txn = doc.transact();
|
|
390
421
|
let sv = txn.state_vector();
|
|
391
422
|
Message::Sync(SyncMessage::SyncStep1(sv)).encode_v1()
|
|
392
423
|
});
|
|
@@ -398,10 +429,11 @@ impl RbAwareness {
|
|
|
398
429
|
fn start(&self) -> Result<RString, Error> {
|
|
399
430
|
let awareness = &self.0;
|
|
400
431
|
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
432
|
+
let awareness = awareness.lock().unwrap();
|
|
401
433
|
let protocol = DefaultProtocol;
|
|
402
434
|
let mut encoder = EncoderV1::new();
|
|
403
435
|
protocol
|
|
404
|
-
.start(awareness, &mut encoder)
|
|
436
|
+
.start(&awareness, &mut encoder)
|
|
405
437
|
.map_err(|e| e.to_string())?;
|
|
406
438
|
Ok(encoder.to_vec())
|
|
407
439
|
})
|
|
@@ -416,9 +448,10 @@ impl RbAwareness {
|
|
|
416
448
|
let awareness = &self.0;
|
|
417
449
|
|
|
418
450
|
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
451
|
+
let mut awareness = awareness.lock().unwrap();
|
|
419
452
|
let protocol = DefaultProtocol;
|
|
420
453
|
let responses = protocol
|
|
421
|
-
.handle(awareness, &data_bytes)
|
|
454
|
+
.handle(&mut awareness, &data_bytes)
|
|
422
455
|
.map_err(|e| e.to_string())?;
|
|
423
456
|
|
|
424
457
|
if responses.is_empty() {
|
|
@@ -446,7 +479,8 @@ impl RbAwareness {
|
|
|
446
479
|
fn encode_state_vector(&self) -> RString {
|
|
447
480
|
let awareness = &self.0;
|
|
448
481
|
let sv = nogvl(move || {
|
|
449
|
-
let
|
|
482
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
483
|
+
let txn = doc.transact();
|
|
450
484
|
txn.state_vector().encode_v1()
|
|
451
485
|
});
|
|
452
486
|
binary_string(&sv)
|
|
@@ -466,7 +500,8 @@ impl RbAwareness {
|
|
|
466
500
|
None => yrs::StateVector::default(),
|
|
467
501
|
Some(bytes) => yrs::StateVector::decode_v1(bytes).map_err(|e| e.to_string())?,
|
|
468
502
|
};
|
|
469
|
-
let
|
|
503
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
504
|
+
let txn = doc.transact();
|
|
470
505
|
Ok(txn.encode_state_as_update_v1(&sv))
|
|
471
506
|
})
|
|
472
507
|
.map_err(runtime_error)?;
|
|
@@ -475,37 +510,47 @@ impl RbAwareness {
|
|
|
475
510
|
|
|
476
511
|
/// Set local awareness state (JSON string)
|
|
477
512
|
fn set_local_state(&self, json: String) -> Result<(), Error> {
|
|
478
|
-
let awareness = &self.0;
|
|
479
513
|
let value: serde_json::Value =
|
|
480
514
|
serde_json::from_str(&json).map_err(|e| runtime_error(e.to_string()))?;
|
|
481
|
-
awareness
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
515
|
+
let awareness = &self.0;
|
|
516
|
+
nogvl(move || -> Result<(), String> {
|
|
517
|
+
awareness
|
|
518
|
+
.lock()
|
|
519
|
+
.unwrap()
|
|
520
|
+
.set_local_state(value)
|
|
521
|
+
.map_err(|e| e.to_string())
|
|
522
|
+
})
|
|
523
|
+
.map_err(runtime_error)
|
|
485
524
|
}
|
|
486
525
|
|
|
487
526
|
/// Get local awareness state as JSON string (or nil if not set)
|
|
488
527
|
fn local_state(&self) -> Option<String> {
|
|
489
528
|
let awareness = &self.0;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
529
|
+
nogvl(move || {
|
|
530
|
+
awareness
|
|
531
|
+
.lock()
|
|
532
|
+
.unwrap()
|
|
533
|
+
.local_state::<serde_json::Value>()
|
|
534
|
+
.map(|v| v.to_string())
|
|
535
|
+
})
|
|
493
536
|
}
|
|
494
537
|
|
|
495
538
|
/// Clear local awareness state
|
|
496
539
|
fn clear_local_state(&self) {
|
|
497
540
|
let awareness = &self.0;
|
|
498
|
-
awareness.clean_local_state();
|
|
541
|
+
nogvl(move || awareness.lock().unwrap().clean_local_state());
|
|
499
542
|
}
|
|
500
543
|
|
|
501
544
|
/// Get awareness update for broadcasting to peers
|
|
502
545
|
fn encode_awareness_update(&self) -> Result<RString, Error> {
|
|
503
546
|
let awareness = &self.0;
|
|
504
|
-
let
|
|
505
|
-
.
|
|
506
|
-
.map_err(|e|
|
|
507
|
-
|
|
508
|
-
|
|
547
|
+
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
548
|
+
let awareness = awareness.lock().unwrap();
|
|
549
|
+
let update = awareness.update().map_err(|e| e.to_string())?;
|
|
550
|
+
Ok(Message::Awareness(update).encode_v1())
|
|
551
|
+
})
|
|
552
|
+
.map_err(runtime_error)?;
|
|
553
|
+
Ok(binary_string(&encoded))
|
|
509
554
|
}
|
|
510
555
|
|
|
511
556
|
/// Apply a raw update to the underlying document
|
|
@@ -514,7 +559,8 @@ impl RbAwareness {
|
|
|
514
559
|
let awareness = &self.0;
|
|
515
560
|
nogvl(move || -> Result<(), String> {
|
|
516
561
|
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
517
|
-
let
|
|
562
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
563
|
+
let mut txn = doc.transact_mut();
|
|
518
564
|
txn.apply_update(update).map_err(|e| e.to_string())
|
|
519
565
|
})
|
|
520
566
|
.map_err(runtime_error)
|
|
@@ -525,15 +571,22 @@ impl RbAwareness {
|
|
|
525
571
|
/// Pure read; does not mutate.
|
|
526
572
|
fn update_ready(&self, update: RString) -> Result<bool, Error> {
|
|
527
573
|
let update_bytes = copy_bytes(update);
|
|
528
|
-
let
|
|
529
|
-
nogvl(move ||
|
|
574
|
+
let awareness = &self.0;
|
|
575
|
+
nogvl(move || {
|
|
576
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
577
|
+
update_is_ready(&doc, &update_bytes)
|
|
578
|
+
})
|
|
579
|
+
.map_err(runtime_error)
|
|
530
580
|
}
|
|
531
581
|
|
|
532
582
|
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
533
583
|
/// missing dependency.
|
|
534
584
|
fn pending(&self) -> bool {
|
|
535
|
-
let
|
|
536
|
-
nogvl(move ||
|
|
585
|
+
let awareness = &self.0;
|
|
586
|
+
nogvl(move || {
|
|
587
|
+
let doc = awareness.lock().unwrap().doc().clone();
|
|
588
|
+
doc_has_pending(&doc)
|
|
589
|
+
})
|
|
537
590
|
}
|
|
538
591
|
|
|
539
592
|
/// Decode the awareness client IDs referenced by a protocol message
|
|
@@ -580,11 +633,13 @@ impl RbAwareness {
|
|
|
580
633
|
fn remove_clients(&self, client_ids: Vec<u64>) -> Result<RString, Error> {
|
|
581
634
|
let awareness = &self.0;
|
|
582
635
|
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
636
|
+
let mut awareness = awareness.lock().unwrap();
|
|
583
637
|
let mut removed = Vec::new();
|
|
584
638
|
for id in client_ids {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
639
|
+
let cid = ClientID::new(id);
|
|
640
|
+
if awareness.meta(cid).is_some() {
|
|
641
|
+
awareness.remove_state(cid);
|
|
642
|
+
removed.push(cid);
|
|
588
643
|
}
|
|
589
644
|
}
|
|
590
645
|
if removed.is_empty() {
|
|
@@ -721,7 +776,7 @@ mod tests {
|
|
|
721
776
|
}
|
|
722
777
|
|
|
723
778
|
fn awareness_frame(client_id: u64) -> Vec<u8> {
|
|
724
|
-
let awareness = Awareness::new(Doc::with_client_id(client_id));
|
|
779
|
+
let mut awareness = Awareness::new(Doc::with_client_id(client_id));
|
|
725
780
|
awareness
|
|
726
781
|
.set_local_state(serde_json::json!({ "user": "alice" }))
|
|
727
782
|
.unwrap();
|
data/lib/yrb_lite/version.rb
CHANGED