yrby 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/Cargo.lock +644 -0
- data/README.md +38 -2
- data/ext/yrby/src/lib.rs +43 -7
- data/ext/yrby/src/protocol.rs +494 -23
- data/ext/yrby/src/read.rs +84 -8
- data/lib/y/version.rb +1 -1
- metadata +2 -4
- data/lib/y/decoder/version.rb +0 -7
- data/lib/y/decoder.rb +0 -66
- data/lib/yrby-decoder.rb +0 -4
data/ext/yrby/src/lib.rs
CHANGED
|
@@ -8,7 +8,10 @@ use yrs::{Doc, GetString, ReadTxn, Transact};
|
|
|
8
8
|
|
|
9
9
|
mod protocol;
|
|
10
10
|
mod read;
|
|
11
|
-
use protocol::{
|
|
11
|
+
use protocol::{
|
|
12
|
+
classify_message, has_pending, integrated_update, merged_doc_update, update_advances_doc,
|
|
13
|
+
update_is_ready,
|
|
14
|
+
};
|
|
12
15
|
|
|
13
16
|
/// Wrapper around yrs Doc.
|
|
14
17
|
///
|
|
@@ -157,9 +160,11 @@ impl RbDoc {
|
|
|
157
160
|
fn read_text(&self, name: String) -> Option<String> {
|
|
158
161
|
let doc = &self.0;
|
|
159
162
|
nogvl(move || {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
// Exactly ONE transaction per call. Opening a second while the
|
|
164
|
+
// first is still held deadlocks against a waiting writer — and
|
|
165
|
+
// inside nogvl that hang can't be interrupted.
|
|
166
|
+
let txn = doc.transact();
|
|
167
|
+
txn.get_text(name.as_str()).map(|t| t.get_string(&txn))
|
|
163
168
|
})
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -188,6 +193,28 @@ impl RbDoc {
|
|
|
188
193
|
})
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
/// True if the doc holds un-integrable pending structs or a pending delete
|
|
197
|
+
/// set — content that couldn't integrate because a causally-prior update is
|
|
198
|
+
/// missing. Such content is a recovery buffer, not document state; it heals if
|
|
199
|
+
/// the missing dependency later arrives. A pure read.
|
|
200
|
+
fn pending(&self) -> bool {
|
|
201
|
+
let doc = &self.0;
|
|
202
|
+
nogvl(move || has_pending(doc))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Like `encode_state_as_update` (full state), but **gap-free**: it excludes
|
|
206
|
+
/// any pending (un-integrable) structs and pending delete set. Use this when
|
|
207
|
+
/// persisting or serving state that other peers will apply — serving pending
|
|
208
|
+
/// content poisons their sync. Non-destructive: this doc keeps its pending, so
|
|
209
|
+
/// a genuine gap still heals if its dependency arrives. (`encode_state_as_update`
|
|
210
|
+
/// stays lossless for raw-update recovery.)
|
|
211
|
+
fn compacted_state_update(&self) -> Result<RString, Error> {
|
|
212
|
+
let doc = &self.0;
|
|
213
|
+
let update = nogvl(move || integrated_update(doc, &yrs::StateVector::default()))
|
|
214
|
+
.map_err(yrb_error)?;
|
|
215
|
+
Ok(binary_string(&update))
|
|
216
|
+
}
|
|
217
|
+
|
|
191
218
|
/// Encode state as update (optionally diffed against a state vector)
|
|
192
219
|
fn encode_state_as_update(&self, args: &[Value]) -> Result<RString, Error> {
|
|
193
220
|
let sv_bytes: Option<Vec<u8>> = if args.is_empty() {
|
|
@@ -263,9 +290,13 @@ impl RbDoc {
|
|
|
263
290
|
match msg {
|
|
264
291
|
Message::Sync(sync_msg) => match sync_msg {
|
|
265
292
|
SyncMessage::SyncStep1(sv) => {
|
|
266
|
-
// Respond with SyncStep2
|
|
267
|
-
|
|
268
|
-
|
|
293
|
+
// Respond with SyncStep2 carrying only *integrated*
|
|
294
|
+
// state. Never hand a peer un-integrable pending
|
|
295
|
+
// structs: the peer would park the same pending
|
|
296
|
+
// forever and the state-vector/content mismatch drives
|
|
297
|
+
// endless resync traffic. (integrated_update is a no-op
|
|
298
|
+
// fast path when nothing is pending.)
|
|
299
|
+
let update = integrated_update(doc, &sv)?;
|
|
269
300
|
let response = Message::Sync(SyncMessage::SyncStep2(update));
|
|
270
301
|
Ok((0, 0, response.encode_v1()))
|
|
271
302
|
}
|
|
@@ -366,6 +397,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
366
397
|
doc_class.define_method("read_text", method!(RbDoc::read_text, 1))?;
|
|
367
398
|
doc_class.define_method("read_xml", method!(RbDoc::read_xml, 1))?;
|
|
368
399
|
doc_class.define_method("read_map", method!(RbDoc::read_map, 1))?;
|
|
400
|
+
doc_class.define_method("pending?", method!(RbDoc::pending, 0))?;
|
|
401
|
+
doc_class.define_method(
|
|
402
|
+
"compacted_state_update",
|
|
403
|
+
method!(RbDoc::compacted_state_update, 0),
|
|
404
|
+
)?;
|
|
369
405
|
doc_class.define_method("update_ready?", method!(RbDoc::update_ready, 1))?;
|
|
370
406
|
doc_class.define_method("update_advances?", method!(RbDoc::update_advances, 1))?;
|
|
371
407
|
doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
|
data/ext/yrby/src/protocol.rs
CHANGED
|
@@ -8,7 +8,7 @@ use yrs::encoding::read::{Cursor, Read};
|
|
|
8
8
|
use yrs::sync::protocol::MessageReader;
|
|
9
9
|
use yrs::sync::{Message, SyncMessage};
|
|
10
10
|
use yrs::updates::decoder::{Decode, DecoderV1};
|
|
11
|
-
use yrs::{Doc, ReadTxn, Transact};
|
|
11
|
+
use yrs::{Doc, ReadTxn, StateVector, Transact, Update, WriteTxn};
|
|
12
12
|
|
|
13
13
|
/// Classify a frame: a non-zero code only for exactly one well-formed message
|
|
14
14
|
/// that consumes the whole buffer (the codes are the match arms below).
|
|
@@ -66,14 +66,42 @@ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String>
|
|
|
66
66
|
Ok(Some(merged))
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/// True if applying `update_bytes` to `doc` would integrate cleanly
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
///
|
|
69
|
+
/// True if applying `update_bytes` to `doc` would integrate cleanly; false if
|
|
70
|
+
/// it would park as pending (a causally-prior update is missing). A pure read.
|
|
71
|
+
///
|
|
72
|
+
/// This must be EXACT: the sync layer records on "ready" and resyncs on "not
|
|
73
|
+
/// ready", and a parked update that slipped through would look like an
|
|
74
|
+
/// already-applied retry downstream — acked and dropped, losing real content.
|
|
75
|
+
///
|
|
76
|
+
/// Clocks alone can't decide it. An update can satisfy every per-client clock
|
|
77
|
+
/// and still fail to integrate: its items may reference other clients' blocks
|
|
78
|
+
/// (origins/parents), and merged updates hide internal gaps behind Skip blocks.
|
|
79
|
+
/// So the clock lower bound serves only as a cheap definitive REJECT; "ready"
|
|
80
|
+
/// is decided by trial-integrating on a throwaway probe seeded with the doc's
|
|
81
|
+
/// integrated state — ready iff nothing parks.
|
|
74
82
|
pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
|
|
75
83
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
76
|
-
|
|
84
|
+
// Partial order: "not covered" includes incomparable — not ready either way.
|
|
85
|
+
let lower_covered = doc.transact().state_vector() >= update.state_vector_lower();
|
|
86
|
+
if !lower_covered {
|
|
87
|
+
return Ok(false);
|
|
88
|
+
}
|
|
89
|
+
// Seed the probe with the doc's INTEGRATED state (gap-free), for two
|
|
90
|
+
// reasons. A lossless seed would replant the doc's own pre-existing
|
|
91
|
+
// pending in the probe, making has_pending true for EVERY update — the
|
|
92
|
+
// verdict must be about this update, not the doc's baggage. And an update
|
|
93
|
+
// whose dependency exists only in that pending buffer is genuinely not
|
|
94
|
+
// ready: recording it would put a gap in the durable log; a resync heals
|
|
95
|
+
// both it and the pending it leans on as one complete delta.
|
|
96
|
+
let seed = integrated_update(doc, &StateVector::default())?;
|
|
97
|
+
let probe = Doc::new();
|
|
98
|
+
{
|
|
99
|
+
let mut txn = probe.transact_mut();
|
|
100
|
+
txn.apply_update(Update::decode_v1(&seed).map_err(|e| e.to_string())?)
|
|
101
|
+
.map_err(|e| e.to_string())?;
|
|
102
|
+
txn.apply_update(update).map_err(|e| e.to_string())?;
|
|
103
|
+
}
|
|
104
|
+
Ok(!has_pending(&probe))
|
|
77
105
|
}
|
|
78
106
|
|
|
79
107
|
/// True if applying `update_bytes` would actually change `doc`, i.e. it carries
|
|
@@ -107,6 +135,16 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
107
135
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
108
136
|
let has_deletes = !update.delete_set().is_empty();
|
|
109
137
|
|
|
138
|
+
// Fast path: blocks beyond the doc's state vector are content the doc
|
|
139
|
+
// lacks — the update advances, no probe needed. The common case (a novel
|
|
140
|
+
// edit) exits here; only retries and ambiguous diffs pay for the probe.
|
|
141
|
+
if !has_deletes {
|
|
142
|
+
let covered = doc.transact().state_vector() >= update.state_vector();
|
|
143
|
+
if !covered {
|
|
144
|
+
return Ok(true);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
// Seed an independent probe with the doc's current state so we can measure the
|
|
111
149
|
// update's effect without mutating the real doc.
|
|
112
150
|
let probe = Doc::new();
|
|
@@ -117,6 +155,10 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
117
155
|
.transact_mut()
|
|
118
156
|
.apply_update(yrs::Update::decode_v1(¤t).map_err(|e| e.to_string())?)
|
|
119
157
|
.map_err(|e| e.to_string())?;
|
|
158
|
+
let pending_before = {
|
|
159
|
+
probe.transact().store().pending_update().is_some()
|
|
160
|
+
|| probe.transact().store().pending_ds().is_some()
|
|
161
|
+
};
|
|
120
162
|
|
|
121
163
|
if has_deletes {
|
|
122
164
|
// Deletes don't move the state vector; compare the full encoded state
|
|
@@ -139,25 +181,73 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
139
181
|
.apply_update(update)
|
|
140
182
|
.map_err(|e| e.to_string())?;
|
|
141
183
|
let after = probe.transact().state_vector();
|
|
142
|
-
|
|
184
|
+
if before != after {
|
|
185
|
+
return Ok(true);
|
|
186
|
+
}
|
|
187
|
+
// An unchanged state vector is ambiguous. Usually it means the doc
|
|
188
|
+
// already had everything in this update (a retry — return false, don't
|
|
189
|
+
// re-record). But it can ALSO mean the update failed to integrate and
|
|
190
|
+
// was stashed as pending, which doesn't move the state vector either.
|
|
191
|
+
// That case is missing content, not a duplicate — returning false would
|
|
192
|
+
// let a caller ack it and drop it. Distinguish the two by whether the
|
|
193
|
+
// probe gained pending. (The sync flow screens gaps out with
|
|
194
|
+
// update_is_ready before calling this; the check guards direct callers.)
|
|
195
|
+
let pending_after = probe.transact().store().pending_update().is_some()
|
|
196
|
+
|| probe.transact().store().pending_ds().is_some();
|
|
197
|
+
Ok(pending_after != pending_before)
|
|
143
198
|
}
|
|
144
199
|
}
|
|
145
200
|
|
|
146
|
-
/// True if the doc holds pending structs or a pending delete set:
|
|
147
|
-
/// couldn't integrate because a
|
|
148
|
-
///
|
|
149
|
-
|
|
150
|
-
pub(crate) fn doc_has_pending(doc: &Doc) -> bool {
|
|
201
|
+
/// True if the doc holds un-integrable pending structs or a pending delete set:
|
|
202
|
+
/// blocks that couldn't integrate because a causally-prior update is missing. A
|
|
203
|
+
/// pure read; does not mutate.
|
|
204
|
+
pub(crate) fn has_pending(doc: &Doc) -> bool {
|
|
151
205
|
let txn = doc.transact();
|
|
152
206
|
txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
|
|
153
207
|
}
|
|
154
208
|
|
|
209
|
+
/// Encode the doc's **integrated** state as a v1 update diffed against `sv`,
|
|
210
|
+
/// excluding any pending (un-integrable) structs and pending delete set.
|
|
211
|
+
///
|
|
212
|
+
/// Pending blocks are a recovery buffer, not document state. Serving them across
|
|
213
|
+
/// the sync boundary hands a peer content it can't integrate, so the peer parks
|
|
214
|
+
/// the same pending forever and the state-vector/content mismatch drives endless
|
|
215
|
+
/// resync traffic. `encode_state_as_update_v1` merges pending back in (see yrs
|
|
216
|
+
/// `merge_pending_v1`), so to get a gap-free encode we rebuild the state into a
|
|
217
|
+
/// throwaway doc and `prune_pending` there before re-encoding.
|
|
218
|
+
///
|
|
219
|
+
/// Non-destructive: the prune happens only on the throwaway copy; `doc` keeps its
|
|
220
|
+
/// pending, so a genuine gap still heals if its missing dependency later arrives.
|
|
221
|
+
pub(crate) fn integrated_update(doc: &Doc, sv: &StateVector) -> Result<Vec<u8>, String> {
|
|
222
|
+
// Pending check and encode share ONE transaction — with two, a concurrent
|
|
223
|
+
// gappy apply_update could slip between them and the encode would serve
|
|
224
|
+
// the very pending this function exists to exclude.
|
|
225
|
+
let full = {
|
|
226
|
+
let txn = doc.transact();
|
|
227
|
+
let store = txn.store();
|
|
228
|
+
// Nothing pending: the direct encode is already gap-free.
|
|
229
|
+
if store.pending_update().is_none() && store.pending_ds().is_none() {
|
|
230
|
+
return Ok(txn.encode_state_as_update_v1(sv));
|
|
231
|
+
}
|
|
232
|
+
txn.encode_state_as_update_v1(&StateVector::default())
|
|
233
|
+
};
|
|
234
|
+
let clean = Doc::new();
|
|
235
|
+
{
|
|
236
|
+
let mut txn = clean.transact_mut();
|
|
237
|
+
txn.apply_update(Update::decode_v1(&full).map_err(|e| e.to_string())?)
|
|
238
|
+
.map_err(|e| e.to_string())?;
|
|
239
|
+
txn.prune_pending();
|
|
240
|
+
}
|
|
241
|
+
let out = clean.transact().encode_state_as_update_v1(sv);
|
|
242
|
+
Ok(out)
|
|
243
|
+
}
|
|
244
|
+
|
|
155
245
|
#[cfg(test)]
|
|
156
246
|
mod tests {
|
|
157
247
|
use super::*;
|
|
158
248
|
use yrs::sync::Awareness;
|
|
159
249
|
use yrs::updates::encoder::Encode;
|
|
160
|
-
use yrs::Text;
|
|
250
|
+
use yrs::{GetString, Text};
|
|
161
251
|
|
|
162
252
|
fn text_update(content: &str) -> Vec<u8> {
|
|
163
253
|
let doc = Doc::new();
|
|
@@ -418,24 +508,405 @@ mod tests {
|
|
|
418
508
|
!update_is_ready(&doc, u3).unwrap(),
|
|
419
509
|
"u3 depends on the missing u2"
|
|
420
510
|
);
|
|
421
|
-
assert!(
|
|
422
|
-
!doc_has_pending(&doc),
|
|
423
|
-
"nothing pending until u3 is applied"
|
|
424
|
-
);
|
|
511
|
+
assert!(!has_pending(&doc), "nothing pending until u3 is applied");
|
|
425
512
|
|
|
426
513
|
// Applying u3 anyway parks it as a pending struct.
|
|
427
514
|
doc.transact_mut()
|
|
428
515
|
.apply_update(yrs::Update::decode_v1(u3).unwrap())
|
|
429
516
|
.unwrap();
|
|
430
|
-
assert!(
|
|
431
|
-
doc_has_pending(&doc),
|
|
432
|
-
"u3 is pending: its parent u2 is missing"
|
|
433
|
-
);
|
|
517
|
+
assert!(has_pending(&doc), "u3 is pending: its parent u2 is missing");
|
|
434
518
|
|
|
435
519
|
// Once u2 arrives (via resync), u3 integrates and pending clears.
|
|
436
520
|
doc.transact_mut()
|
|
437
521
|
.apply_update(yrs::Update::decode_v1(u2).unwrap())
|
|
438
522
|
.unwrap();
|
|
439
|
-
assert!(!
|
|
523
|
+
assert!(!has_pending(&doc), "u2 arrived; u3 integrated");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Build a cross-client-origin gap: client C creates "abc"; client A applies
|
|
527
|
+
// it and types between C's characters, so A's delta references C's blocks as
|
|
528
|
+
// origins. Returns (c_update, a_delta). On a doc missing `c_update`, the
|
|
529
|
+
// per-client clock lower bound of `a_delta` is satisfied (A starts at clock
|
|
530
|
+
// 0) but integration parks — the case a clock-only readiness check misses.
|
|
531
|
+
fn cross_client_origin_gap() -> (Vec<u8>, Vec<u8>) {
|
|
532
|
+
let c = Doc::new();
|
|
533
|
+
let ct = c.get_or_insert_text("t");
|
|
534
|
+
ct.insert(&mut c.transact_mut(), 0, "abc");
|
|
535
|
+
let c_update = c
|
|
536
|
+
.transact()
|
|
537
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
538
|
+
|
|
539
|
+
let a = Doc::new();
|
|
540
|
+
a.transact_mut()
|
|
541
|
+
.apply_update(yrs::Update::decode_v1(&c_update).unwrap())
|
|
542
|
+
.unwrap();
|
|
543
|
+
let sv_before = a.transact().state_vector();
|
|
544
|
+
let at = a.get_or_insert_text("t");
|
|
545
|
+
at.insert(&mut a.transact_mut(), 1, "X"); // between C's chars
|
|
546
|
+
let a_delta = a.transact().encode_state_as_update_v1(&sv_before);
|
|
547
|
+
(c_update, a_delta)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn cross_client_origin_gap_is_not_ready() {
|
|
552
|
+
let (c_update, a_delta) = cross_client_origin_gap();
|
|
553
|
+
|
|
554
|
+
// A server that never saw C's content: the clock lower bound passes, but
|
|
555
|
+
// the update can't integrate — it must NOT be ready (previously it was,
|
|
556
|
+
// and the downstream advances? probe then acked-and-dropped it).
|
|
557
|
+
let server = Doc::new();
|
|
558
|
+
assert!(
|
|
559
|
+
!update_is_ready(&server, &a_delta).unwrap(),
|
|
560
|
+
"a delta with unmet cross-client origins is not ready"
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Once the server has C's content, the same delta is ready and advances.
|
|
564
|
+
server
|
|
565
|
+
.transact_mut()
|
|
566
|
+
.apply_update(yrs::Update::decode_v1(&c_update).unwrap())
|
|
567
|
+
.unwrap();
|
|
568
|
+
assert!(update_is_ready(&server, &a_delta).unwrap());
|
|
569
|
+
assert!(update_advances_doc(&server, &a_delta).unwrap());
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
#[test]
|
|
573
|
+
fn merged_update_with_internal_skip_gap_is_not_ready() {
|
|
574
|
+
// Merging u1 and u3 (u2 missing) yields one update with a Skip block; its
|
|
575
|
+
// clock lower bound is u1's start, but the post-Skip blocks can't
|
|
576
|
+
// integrate on a doc that lacks u2.
|
|
577
|
+
let src = Doc::new();
|
|
578
|
+
let txt = src.get_or_insert_text("t");
|
|
579
|
+
let mut deltas: Vec<Vec<u8>> = Vec::new();
|
|
580
|
+
let mut prev = yrs::StateVector::default();
|
|
581
|
+
for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
|
|
582
|
+
txt.insert(&mut src.transact_mut(), i as u32, ch);
|
|
583
|
+
deltas.push(src.transact().encode_state_as_update_v1(&prev));
|
|
584
|
+
prev = src.transact().state_vector();
|
|
585
|
+
}
|
|
586
|
+
let merged = yrs::merge_updates_v1([deltas[0].as_slice(), deltas[2].as_slice()]).unwrap();
|
|
587
|
+
|
|
588
|
+
let server = Doc::new();
|
|
589
|
+
assert!(
|
|
590
|
+
!update_is_ready(&server, &merged).unwrap(),
|
|
591
|
+
"the post-Skip blocks depend on the missing u2"
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#[test]
|
|
596
|
+
fn a_doc_with_legacy_pending_still_accepts_healthy_updates() {
|
|
597
|
+
// Why update_is_ready seeds its probe with the INTEGRATED state: with a
|
|
598
|
+
// lossless seed, the doc's own pre-existing pending would park in the
|
|
599
|
+
// probe and every verdict would come back "not ready" — a server with
|
|
600
|
+
// one legacy gap would reject every healthy keystroke forever.
|
|
601
|
+
let (_first, dependent) = gap_pair();
|
|
602
|
+
let doc = Doc::new();
|
|
603
|
+
doc.transact_mut()
|
|
604
|
+
.apply_update(yrs::Update::decode_v1(&dependent).unwrap())
|
|
605
|
+
.unwrap();
|
|
606
|
+
assert!(has_pending(&doc), "the doc carries a legacy parked gap");
|
|
607
|
+
|
|
608
|
+
// A healthy, self-contained update from an unrelated client.
|
|
609
|
+
let healthy = {
|
|
610
|
+
let d = Doc::new();
|
|
611
|
+
let t = d.get_or_insert_text("other");
|
|
612
|
+
t.insert(&mut d.transact_mut(), 0, "hello");
|
|
613
|
+
let txn = d.transact();
|
|
614
|
+
txn.encode_state_as_update_v1(&yrs::StateVector::default())
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
assert!(
|
|
618
|
+
update_is_ready(&doc, &healthy).unwrap(),
|
|
619
|
+
"legacy pending must not veto unrelated healthy updates"
|
|
620
|
+
);
|
|
621
|
+
assert!(update_advances_doc(&doc, &healthy).unwrap());
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
#[test]
|
|
625
|
+
fn an_update_depending_only_on_pending_content_is_not_ready() {
|
|
626
|
+
// The other half of the integrated-only seed: a dependency that exists
|
|
627
|
+
// solely in the doc's pending buffer doesn't count — recording such an
|
|
628
|
+
// update would put a gap in the durable log. Not ready; resync heals
|
|
629
|
+
// both as one complete delta.
|
|
630
|
+
let src = Doc::new();
|
|
631
|
+
let txt = src.get_or_insert_text("t");
|
|
632
|
+
let mut deltas: Vec<Vec<u8>> = Vec::new();
|
|
633
|
+
let mut prev = yrs::StateVector::default();
|
|
634
|
+
for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
|
|
635
|
+
txt.insert(&mut src.transact_mut(), i as u32, ch);
|
|
636
|
+
deltas.push(src.transact().encode_state_as_update_v1(&prev));
|
|
637
|
+
prev = src.transact().state_vector();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The doc holds u2 only as PENDING (u1 never arrived); u3 depends on u2.
|
|
641
|
+
let doc = Doc::new();
|
|
642
|
+
doc.transact_mut()
|
|
643
|
+
.apply_update(yrs::Update::decode_v1(&deltas[1]).unwrap())
|
|
644
|
+
.unwrap();
|
|
645
|
+
assert!(has_pending(&doc), "u2 parked without u1");
|
|
646
|
+
|
|
647
|
+
assert!(
|
|
648
|
+
!update_is_ready(&doc, &deltas[2]).unwrap(),
|
|
649
|
+
"a dependency satisfied only by pending content is not ready"
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#[test]
|
|
654
|
+
fn update_advances_reports_true_when_the_update_would_park() {
|
|
655
|
+
// Defense in depth for callers using advances? without the ready gate: a
|
|
656
|
+
// gappy update parks pending — that changes the doc, so it advances (it
|
|
657
|
+
// must never be misread as an already-applied retry and dropped).
|
|
658
|
+
let (_c_update, a_delta) = cross_client_origin_gap();
|
|
659
|
+
let server = Doc::new();
|
|
660
|
+
assert!(
|
|
661
|
+
update_advances_doc(&server, &a_delta).unwrap(),
|
|
662
|
+
"a parked update is not a duplicate"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Build a causal gap: `first` inserts "a", `dependent` inserts "b" after it,
|
|
667
|
+
// so `dependent` alone parks as pending on a doc that lacks `first`.
|
|
668
|
+
fn gap_pair() -> (Vec<u8>, Vec<u8>) {
|
|
669
|
+
let src = Doc::new();
|
|
670
|
+
let txt = src.get_or_insert_text("notepad");
|
|
671
|
+
txt.insert(&mut src.transact_mut(), 0, "a");
|
|
672
|
+
let first = src
|
|
673
|
+
.transact()
|
|
674
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
675
|
+
let sv = src.transact().state_vector();
|
|
676
|
+
txt.insert(&mut src.transact_mut(), 1, "b");
|
|
677
|
+
let dependent = src.transact().encode_state_as_update_v1(&sv);
|
|
678
|
+
(first, dependent)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#[test]
|
|
682
|
+
fn integrated_update_strips_pending_and_is_non_destructive() {
|
|
683
|
+
let (_first, dependent) = gap_pair();
|
|
684
|
+
let doc = Doc::new();
|
|
685
|
+
doc.transact_mut()
|
|
686
|
+
.apply_update(yrs::Update::decode_v1(&dependent).unwrap())
|
|
687
|
+
.unwrap();
|
|
688
|
+
assert!(has_pending(&doc), "the gappy update parked as pending");
|
|
689
|
+
|
|
690
|
+
// encode_state_as_update carries the pending; integrated_update does not.
|
|
691
|
+
let full = doc
|
|
692
|
+
.transact()
|
|
693
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
694
|
+
let gap_free = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
695
|
+
assert_ne!(full, gap_free, "integrated_update drops the pending bytes");
|
|
696
|
+
|
|
697
|
+
// Applying the gap-free encode to a fresh peer must NOT poison it.
|
|
698
|
+
let peer = Doc::new();
|
|
699
|
+
peer.transact_mut()
|
|
700
|
+
.apply_update(yrs::Update::decode_v1(&gap_free).unwrap())
|
|
701
|
+
.unwrap();
|
|
702
|
+
assert!(
|
|
703
|
+
!has_pending(&peer),
|
|
704
|
+
"peer got no pending from the gap-free state"
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Non-destructive: the source doc keeps its pending (so it can still heal).
|
|
708
|
+
assert!(
|
|
709
|
+
has_pending(&doc),
|
|
710
|
+
"integrated_update did not mutate the source"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
#[test]
|
|
715
|
+
fn integrated_update_fast_path_matches_direct_encode_when_clean() {
|
|
716
|
+
// No pending -> byte-identical to encode_state_as_update (zero-copy path).
|
|
717
|
+
let (first, _dependent) = gap_pair();
|
|
718
|
+
let doc = Doc::new();
|
|
719
|
+
doc.transact_mut()
|
|
720
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
721
|
+
.unwrap();
|
|
722
|
+
assert!(!has_pending(&doc));
|
|
723
|
+
let direct = doc
|
|
724
|
+
.transact()
|
|
725
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
726
|
+
let via = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
727
|
+
assert_eq!(direct, via);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#[test]
|
|
731
|
+
fn a_healed_gap_serves_its_content() {
|
|
732
|
+
// After the missing dependency arrives, the (formerly pending) content is
|
|
733
|
+
// integrated and integrated_update includes it.
|
|
734
|
+
let (first, dependent) = gap_pair();
|
|
735
|
+
let doc = Doc::new();
|
|
736
|
+
doc.transact_mut()
|
|
737
|
+
.apply_update(yrs::Update::decode_v1(&dependent).unwrap())
|
|
738
|
+
.unwrap();
|
|
739
|
+
doc.transact_mut()
|
|
740
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
741
|
+
.unwrap();
|
|
742
|
+
assert!(!has_pending(&doc), "gap healed once first arrived");
|
|
743
|
+
let gap_free = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
744
|
+
let peer = Doc::new();
|
|
745
|
+
peer.transact_mut()
|
|
746
|
+
.apply_update(yrs::Update::decode_v1(&gap_free).unwrap())
|
|
747
|
+
.unwrap();
|
|
748
|
+
assert_eq!(
|
|
749
|
+
peer.get_or_insert_text("notepad")
|
|
750
|
+
.get_string(&peer.transact()),
|
|
751
|
+
"ab"
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// A gappy insert from its own independent client: inserts two chars and
|
|
756
|
+
// returns only the second delta, which depends on the (missing) first.
|
|
757
|
+
fn independent_gappy_insert() -> Vec<u8> {
|
|
758
|
+
let src = Doc::new();
|
|
759
|
+
let txt = src.get_or_insert_text("notepad");
|
|
760
|
+
txt.insert(&mut src.transact_mut(), 0, "x");
|
|
761
|
+
let sv = src.transact().state_vector();
|
|
762
|
+
txt.insert(&mut src.transact_mut(), 1, "y");
|
|
763
|
+
let txn = src.transact();
|
|
764
|
+
txn.encode_state_as_update_v1(&sv)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
#[test]
|
|
768
|
+
fn integrated_update_keeps_content_and_drops_pending_when_mixed() {
|
|
769
|
+
// The realistic case: a doc with real integrated content AND a pending
|
|
770
|
+
// struct. Pruning must keep the content and drop only the pending.
|
|
771
|
+
let (first, _dep) = gap_pair();
|
|
772
|
+
let doc = Doc::new();
|
|
773
|
+
doc.transact_mut()
|
|
774
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
775
|
+
.unwrap(); // integrated "a"
|
|
776
|
+
doc.transact_mut()
|
|
777
|
+
.apply_update(yrs::Update::decode_v1(&independent_gappy_insert()).unwrap())
|
|
778
|
+
.unwrap(); // + a pending struct from another client
|
|
779
|
+
assert!(has_pending(&doc));
|
|
780
|
+
|
|
781
|
+
let gap_free = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
782
|
+
let peer = Doc::new();
|
|
783
|
+
peer.transact_mut()
|
|
784
|
+
.apply_update(yrs::Update::decode_v1(&gap_free).unwrap())
|
|
785
|
+
.unwrap();
|
|
786
|
+
assert_eq!(
|
|
787
|
+
peer.get_or_insert_text("notepad")
|
|
788
|
+
.get_string(&peer.transact()),
|
|
789
|
+
"a",
|
|
790
|
+
"kept the integrated content"
|
|
791
|
+
);
|
|
792
|
+
assert!(!has_pending(&peer), "dropped the pending");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
#[test]
|
|
796
|
+
fn integrated_update_diffs_against_a_peer_sv_and_excludes_pending() {
|
|
797
|
+
// The production signature: `handle_sync_message` calls
|
|
798
|
+
// integrated_update(doc, peer_sv). A peer already holding the integrated
|
|
799
|
+
// content should get a diff carrying no new content and no pending.
|
|
800
|
+
let (first, _dep) = gap_pair();
|
|
801
|
+
let server = Doc::new();
|
|
802
|
+
server
|
|
803
|
+
.transact_mut()
|
|
804
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
805
|
+
.unwrap();
|
|
806
|
+
server
|
|
807
|
+
.transact_mut()
|
|
808
|
+
.apply_update(yrs::Update::decode_v1(&independent_gappy_insert()).unwrap())
|
|
809
|
+
.unwrap();
|
|
810
|
+
|
|
811
|
+
let peer = Doc::new();
|
|
812
|
+
peer.transact_mut()
|
|
813
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
814
|
+
.unwrap();
|
|
815
|
+
let peer_sv = peer.transact().state_vector();
|
|
816
|
+
|
|
817
|
+
let diff = integrated_update(&server, &peer_sv).unwrap();
|
|
818
|
+
peer.transact_mut()
|
|
819
|
+
.apply_update(yrs::Update::decode_v1(&diff).unwrap())
|
|
820
|
+
.unwrap();
|
|
821
|
+
assert_eq!(
|
|
822
|
+
peer.get_or_insert_text("notepad")
|
|
823
|
+
.get_string(&peer.transact()),
|
|
824
|
+
"a"
|
|
825
|
+
);
|
|
826
|
+
assert!(!has_pending(&peer), "the diff carried no pending");
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[test]
|
|
830
|
+
fn integrated_update_never_serves_pending_under_concurrent_gappy_applies() {
|
|
831
|
+
// Invariant under contention: while a writer parks and heals a gappy
|
|
832
|
+
// update in a loop, every integrated_update encode must be pending-free
|
|
833
|
+
// for a fresh peer.
|
|
834
|
+
//
|
|
835
|
+
// Scope: this can't hit the original check-vs-encode race (its window
|
|
836
|
+
// is nanoseconds; never reproduced even at 20k iterations) — that fix
|
|
837
|
+
// is guaranteed by using a single transaction. What this catches is
|
|
838
|
+
// coarser: encoding outside the lock, or a fast path skipping the
|
|
839
|
+
// pending check.
|
|
840
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
841
|
+
use std::sync::Arc as StdArc;
|
|
842
|
+
|
|
843
|
+
let (first, dependent) = gap_pair();
|
|
844
|
+
let doc = StdArc::new(Doc::new());
|
|
845
|
+
let stop = StdArc::new(AtomicBool::new(false));
|
|
846
|
+
|
|
847
|
+
let writer = {
|
|
848
|
+
let doc = StdArc::clone(&doc);
|
|
849
|
+
let stop = StdArc::clone(&stop);
|
|
850
|
+
let dependent = dependent.clone();
|
|
851
|
+
let first = first.clone();
|
|
852
|
+
std::thread::spawn(move || {
|
|
853
|
+
while !stop.load(Ordering::Relaxed) {
|
|
854
|
+
// Park a pending struct, then heal it, over and over — the
|
|
855
|
+
// encode below keeps racing both transitions.
|
|
856
|
+
doc.transact_mut()
|
|
857
|
+
.apply_update(yrs::Update::decode_v1(&dependent).unwrap())
|
|
858
|
+
.unwrap();
|
|
859
|
+
doc.transact_mut()
|
|
860
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
861
|
+
.unwrap();
|
|
862
|
+
}
|
|
863
|
+
})
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
for _ in 0..500 {
|
|
867
|
+
let encoded = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
868
|
+
let peer = Doc::new();
|
|
869
|
+
peer.transact_mut()
|
|
870
|
+
.apply_update(yrs::Update::decode_v1(&encoded).unwrap())
|
|
871
|
+
.unwrap();
|
|
872
|
+
assert!(
|
|
873
|
+
!has_pending(&peer),
|
|
874
|
+
"an integrated_update encode leaked pending to a peer"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
stop.store(true, Ordering::Relaxed);
|
|
878
|
+
writer.join().unwrap();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
#[test]
|
|
882
|
+
fn integrated_update_strips_a_pending_delete_set() {
|
|
883
|
+
// A deletion whose target struct is absent parks as a pending *delete
|
|
884
|
+
// set* -- the delete-side counterpart to a pending struct.
|
|
885
|
+
let src = Doc::new();
|
|
886
|
+
let txt = src.get_or_insert_text("notepad");
|
|
887
|
+
txt.insert(&mut src.transact_mut(), 0, "z");
|
|
888
|
+
let sv = src.transact().state_vector();
|
|
889
|
+
txt.remove_range(&mut src.transact_mut(), 0, 1);
|
|
890
|
+
let deletion = src.transact().encode_state_as_update_v1(&sv); // delete-only
|
|
891
|
+
|
|
892
|
+
let doc = Doc::new();
|
|
893
|
+
doc.transact_mut()
|
|
894
|
+
.apply_update(yrs::Update::decode_v1(&deletion).unwrap())
|
|
895
|
+
.unwrap();
|
|
896
|
+
assert!(
|
|
897
|
+
has_pending(&doc),
|
|
898
|
+
"the orphan deletion parked as a pending delete set"
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
let gap_free = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
902
|
+
let peer = Doc::new();
|
|
903
|
+
peer.transact_mut()
|
|
904
|
+
.apply_update(yrs::Update::decode_v1(&gap_free).unwrap())
|
|
905
|
+
.unwrap();
|
|
906
|
+
assert!(!has_pending(&peer), "the pending delete set was not served");
|
|
907
|
+
assert!(
|
|
908
|
+
has_pending(&doc),
|
|
909
|
+
"non-destructive: source keeps its pending"
|
|
910
|
+
);
|
|
440
911
|
}
|
|
441
912
|
}
|