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.
@@ -0,0 +1,330 @@
1
+ // Pure rust protocol helpers (ie, no Ruby interop).
2
+ //
3
+ // CLIENT IDs ARE NOT VALIDATED HERE -- on purpose. We deliberately don't police it -- every
4
+ // legitimate peer (browser Yjs, and yrs's own `ClientID::random`) already emits
5
+ // 53-bit ids, so it's the client's responsibility not to send a bad one, and we
6
+ // don't want to own that logic.
7
+ use yrs::encoding::read::{Cursor, Read};
8
+ use yrs::sync::protocol::MessageReader;
9
+ use yrs::sync::{Message, SyncMessage};
10
+ use yrs::updates::decoder::{Decode, DecoderV1};
11
+ use yrs::{Doc, ReadTxn, Transact};
12
+
13
+ /// Classify a frame: a non-zero code only for exactly one well-formed message
14
+ /// that consumes the whole buffer (see `RbAwareness::message_kind` for codes).
15
+ pub(crate) fn classify_message(bytes: &[u8]) -> u8 {
16
+ let mut decoder = DecoderV1::new(Cursor::new(bytes));
17
+ let msg = match Message::decode(&mut decoder) {
18
+ Ok(msg) => msg,
19
+ Err(_) => return 0, // empty or malformed
20
+ };
21
+ // Any remaining byte means a second message or trailing garbage.
22
+ if decoder.read_u8().is_ok() {
23
+ return 0;
24
+ }
25
+ match msg {
26
+ Message::Sync(SyncMessage::SyncStep1(_)) => 1,
27
+ Message::Sync(SyncMessage::SyncStep2(_)) | Message::Sync(SyncMessage::Update(_)) => 2,
28
+ Message::Awareness(_) => 3,
29
+ Message::AwarenessQuery => 4,
30
+ _ => 0, // Auth / Custom: not part of our model
31
+ }
32
+ }
33
+
34
+ /// Merge the document-update deltas (Update / SyncStep2 payloads) carried by a
35
+ /// frame into one update, or `None` if the frame carries no document change
36
+ /// (a request, an awareness update, or a no-op handshake SyncStep2).
37
+ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
38
+ let mut decoder = DecoderV1::new(Cursor::new(bytes));
39
+ let mut updates: Vec<Vec<u8>> = Vec::new();
40
+ for msg in MessageReader::new(&mut decoder) {
41
+ match msg.map_err(|e| e.to_string())? {
42
+ Message::Sync(SyncMessage::Update(u)) | Message::Sync(SyncMessage::SyncStep2(u)) => {
43
+ updates.push(u)
44
+ }
45
+ _ => {}
46
+ }
47
+ }
48
+ let merged = match updates.len() {
49
+ 0 => return Ok(None),
50
+ 1 => updates.pop().unwrap(),
51
+ _ => yrs::merge_updates_v1(&updates).map_err(|e| e.to_string())?,
52
+ };
53
+ let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
54
+ // A genuine no-op (e.g. the empty SyncStep2 in an opening handshake) carries
55
+ // no structs, no deletes, and no dependencies. We must NOT treat a causally-
56
+ // pending update as a no-op: since yrs 0.26 such an update reports an empty
57
+ // state_vector (its structs can't integrate yet), but it still carries
58
+ // content and a non-empty lower bound (the deps it's waiting on). Dropping it
59
+ // here would silently swallow a gappy update instead of rejecting + resyncing.
60
+ if update.state_vector().is_empty()
61
+ && update.delete_set().is_empty()
62
+ && update.state_vector_lower().is_empty()
63
+ {
64
+ return Ok(None);
65
+ }
66
+ Ok(Some(merged))
67
+ }
68
+
69
+ /// True if applying `update_bytes` to `doc` would integrate cleanly: every
70
+ /// dependency the update references is already present (the doc's state vector
71
+ /// covers the update's lower bound). A pure read; does not mutate the doc.
72
+ /// When false, applying it would park a pending struct -- the signal that an
73
+ /// earlier, causally-prior update is missing.
74
+ pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
75
+ let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
76
+ Ok(doc.transact().state_vector() >= update.state_vector_lower())
77
+ }
78
+
79
+ /// True if applying `update_bytes` would actually change `doc` -- i.e. it carries
80
+ /// content the doc doesn't already have. Lets the server make durable side
81
+ /// effects exactly-once: a lost-ack retry re-sends an update the server already
82
+ /// applied; that retry is causally ready (so `update_is_ready` is true) but must
83
+ /// NOT re-run `on_change`.
84
+ ///
85
+ /// We can't read the update's own state vector to decide this: yrs reports an
86
+ /// EMPTY state_vector() for a causally-pending diff (e.g. a resync delta whose
87
+ /// structs depend on updates the doc has but the standalone update doesn't),
88
+ /// which would look identical to a no-op. So measure the real effect: seed an
89
+ /// independent probe with the doc's current state, apply the update there, and
90
+ /// see whether the state vector grew. Deletes don't move the state vector, so we
91
+ /// can't cheaply prove a delete-bearing update is a duplicate -- we
92
+ /// conservatively report it as advancing (record it). That can still
93
+ /// double-record a pure-delete retry, but it NEVER drops a real deletion, which
94
+ /// is the safe direction. Assumes the update is already causally ready.
95
+ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
96
+ let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
97
+ if !update.delete_set().is_empty() {
98
+ return Ok(true); // can't cheaply prove a delete is a duplicate; record it
99
+ }
100
+ let probe = Doc::new();
101
+ let current = doc
102
+ .transact()
103
+ .encode_state_as_update_v1(&yrs::StateVector::default());
104
+ probe
105
+ .transact_mut()
106
+ .apply_update(yrs::Update::decode_v1(&current).map_err(|e| e.to_string())?)
107
+ .map_err(|e| e.to_string())?;
108
+ let before = probe.transact().state_vector();
109
+ probe
110
+ .transact_mut()
111
+ .apply_update(update)
112
+ .map_err(|e| e.to_string())?;
113
+ let after = probe.transact().state_vector();
114
+ Ok(after != before)
115
+ }
116
+
117
+ /// True if the doc holds pending structs or a pending delete set -- blocks that
118
+ /// couldn't integrate because a dependency is missing. Used as a backstop after
119
+ /// loading from storage: leftover pending means the stored log has a causal gap.
120
+ pub(crate) fn doc_has_pending(doc: &Doc) -> bool {
121
+ let txn = doc.transact();
122
+ txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
123
+ }
124
+
125
+ #[cfg(test)]
126
+ mod tests {
127
+ use super::*;
128
+ use yrs::sync::Awareness;
129
+ use yrs::updates::encoder::Encode;
130
+ use yrs::Text;
131
+
132
+ fn text_update(content: &str) -> Vec<u8> {
133
+ let doc = Doc::new();
134
+ let text = doc.get_or_insert_text("content");
135
+ text.insert(&mut doc.transact_mut(), 0, content);
136
+ let update = doc
137
+ .transact()
138
+ .encode_state_as_update_v1(&yrs::StateVector::default());
139
+ update
140
+ }
141
+
142
+ fn update_frame(content: &str) -> Vec<u8> {
143
+ Message::Sync(SyncMessage::Update(text_update(content))).encode_v1()
144
+ }
145
+
146
+ fn step1_frame() -> Vec<u8> {
147
+ Message::Sync(SyncMessage::SyncStep1(yrs::StateVector::default())).encode_v1()
148
+ }
149
+
150
+ fn awareness_frame(client_id: u64) -> Vec<u8> {
151
+ let mut awareness = Awareness::new(Doc::with_client_id(client_id));
152
+ awareness
153
+ .set_local_state(serde_json::json!({ "user": "alice" }))
154
+ .unwrap();
155
+ Message::Awareness(awareness.update().unwrap()).encode_v1()
156
+ }
157
+
158
+ #[test]
159
+ fn classify_accepts_clean_single_messages() {
160
+ assert_eq!(classify_message(&step1_frame()), 1);
161
+ assert_eq!(classify_message(&update_frame("hi")), 2);
162
+ assert_eq!(classify_message(&awareness_frame(7)), 3);
163
+ assert_eq!(classify_message(&Message::AwarenessQuery.encode_v1()), 4);
164
+ }
165
+
166
+ #[test]
167
+ fn classify_rejects_unsafe_frames() {
168
+ assert_eq!(classify_message(b""), 0, "empty");
169
+ assert_eq!(classify_message(&[0xff, 0xff, 0xff]), 0, "garbage");
170
+ assert_eq!(classify_message(&[0x63, 0x63, 0x63]), 0, "unknown type");
171
+
172
+ let mut two = update_frame("a");
173
+ two.extend(awareness_frame(1)); // two messages packed together
174
+ assert_eq!(classify_message(&two), 0, "multi-message");
175
+
176
+ let mut trailing = update_frame("a");
177
+ trailing.extend_from_slice(&[0xde, 0xad]);
178
+ assert_eq!(classify_message(&trailing), 0, "trailing garbage");
179
+
180
+ let frame = update_frame("hello");
181
+ assert_eq!(classify_message(&frame[..frame.len() / 2]), 0, "truncated");
182
+ }
183
+
184
+ #[test]
185
+ fn update_advances_is_false_for_an_already_applied_retry() {
186
+ let doc = Doc::new();
187
+ let upd = text_update("hello");
188
+
189
+ // Against a doc that doesn't have it yet, the update advances.
190
+ assert!(
191
+ update_advances_doc(&doc, &upd).unwrap(),
192
+ "new content advances"
193
+ );
194
+
195
+ // Apply it, then the byte-identical retry no longer advances.
196
+ doc.transact_mut()
197
+ .apply_update(yrs::Update::decode_v1(&upd).unwrap())
198
+ .unwrap();
199
+ assert!(
200
+ !update_advances_doc(&doc, &upd).unwrap(),
201
+ "an already-applied retry does not advance"
202
+ );
203
+
204
+ // A genuinely new insert (from a different client) still advances.
205
+ let more = text_update("world");
206
+ assert!(
207
+ update_advances_doc(&doc, &more).unwrap(),
208
+ "different new content advances"
209
+ );
210
+ }
211
+
212
+ #[test]
213
+ fn update_advances_handles_a_dependent_diff_update() {
214
+ // A causally-pending diff (its structs depend on content the doc already
215
+ // has) reports an EMPTY state_vector() in isolation -- a naive check would
216
+ // misread it as a no-op. Verify the trial-apply gets it right.
217
+ let doc = Doc::new();
218
+ let text = doc.get_or_insert_text("content");
219
+ text.insert(&mut doc.transact_mut(), 0, "a");
220
+ let a_update = doc
221
+ .transact()
222
+ .encode_state_as_update_v1(&yrs::StateVector::default());
223
+ let sv_a = doc.transact().state_vector();
224
+ text.insert(&mut doc.transact_mut(), 1, "b");
225
+ let diff = doc.transact().encode_state_as_update_v1(&sv_a); // depends on "a"
226
+
227
+ // A server that has only "a".
228
+ let server = Doc::new();
229
+ server
230
+ .transact_mut()
231
+ .apply_update(yrs::Update::decode_v1(&a_update).unwrap())
232
+ .unwrap();
233
+
234
+ assert!(
235
+ update_advances_doc(&server, &diff).unwrap(),
236
+ "a dependent diff carrying new content advances"
237
+ );
238
+ server
239
+ .transact_mut()
240
+ .apply_update(yrs::Update::decode_v1(&diff).unwrap())
241
+ .unwrap();
242
+ assert!(
243
+ !update_advances_doc(&server, &diff).unwrap(),
244
+ "the byte-identical retry of that diff does not advance"
245
+ );
246
+ }
247
+
248
+ #[test]
249
+ fn merged_doc_update_extracts_and_skips_no_ops() {
250
+ // A document update yields a delta that reconstructs the content.
251
+ let delta = merged_doc_update(&update_frame("hello"))
252
+ .unwrap()
253
+ .expect("a document update");
254
+ let doc = Doc::new();
255
+ doc.transact_mut()
256
+ .apply_update(yrs::Update::decode_v1(&delta).unwrap())
257
+ .unwrap();
258
+ // The delta carried real content, so applying it advances the doc.
259
+ assert!(!doc.transact().state_vector().is_empty());
260
+
261
+ // A SyncStep1 request carries no document change.
262
+ assert!(merged_doc_update(&step1_frame()).unwrap().is_none());
263
+
264
+ // An empty SyncStep2 (no new structs) is a no-op.
265
+ let empty = Message::Sync(SyncMessage::SyncStep2(
266
+ Doc::new()
267
+ .transact()
268
+ .encode_state_as_update_v1(&yrs::StateVector::default()),
269
+ ))
270
+ .encode_v1();
271
+ assert!(merged_doc_update(&empty).unwrap().is_none());
272
+ }
273
+
274
+ #[test]
275
+ fn merged_doc_update_merges_multiple_updates() {
276
+ // Two updates from different clients packed in one frame merge into one.
277
+ let mut frame = update_frame("a");
278
+ frame.extend(update_frame("b"));
279
+ let merged = merged_doc_update(&frame).unwrap().expect("merged update");
280
+
281
+ // The merged update must decode cleanly as a single update.
282
+ assert!(yrs::Update::decode_v1(&merged).is_ok());
283
+ }
284
+
285
+ #[test]
286
+ fn update_readiness_and_pending_detect_a_causal_gap() {
287
+ // Three sequential single-char inserts from one client: A, then B, then
288
+ // C. Each delta depends on the previous, so C can't integrate without B.
289
+ let src = Doc::new();
290
+ let txt = src.get_or_insert_text("t");
291
+ let mut deltas: Vec<Vec<u8>> = Vec::new();
292
+ let mut prev = yrs::StateVector::default();
293
+ for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
294
+ txt.insert(&mut src.transact_mut(), i as u32, ch);
295
+ deltas.push(src.transact().encode_state_as_update_v1(&prev));
296
+ prev = src.transact().state_vector();
297
+ }
298
+ let (u1, u2, u3) = (&deltas[0], &deltas[1], &deltas[2]);
299
+
300
+ // A doc holding only u1 (u2 was lost in transit / its record failed):
301
+ let doc = Doc::new();
302
+ doc.transact_mut()
303
+ .apply_update(yrs::Update::decode_v1(u1).unwrap())
304
+ .unwrap();
305
+ assert!(update_is_ready(&doc, u1).unwrap(), "u1 has no missing deps");
306
+ assert!(
307
+ !update_is_ready(&doc, u3).unwrap(),
308
+ "u3 depends on the missing u2"
309
+ );
310
+ assert!(
311
+ !doc_has_pending(&doc),
312
+ "nothing pending until u3 is applied"
313
+ );
314
+
315
+ // Applying u3 anyway parks it as a pending struct.
316
+ doc.transact_mut()
317
+ .apply_update(yrs::Update::decode_v1(u3).unwrap())
318
+ .unwrap();
319
+ assert!(
320
+ doc_has_pending(&doc),
321
+ "u3 is pending: its parent u2 is missing"
322
+ );
323
+
324
+ // Once u2 arrives (via resync), u3 integrates and pending clears.
325
+ doc.transact_mut()
326
+ .apply_update(yrs::Update::decode_v1(u2).unwrap())
327
+ .unwrap();
328
+ assert!(!doc_has_pending(&doc), "u2 arrived; u3 integrated");
329
+ }
330
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YrbLite
4
- VERSION = "0.1.0.beta5"
4
+ VERSION = "0.1.0.beta7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrb-lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.beta5
4
+ version: 0.1.0.beta7
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara
@@ -83,6 +83,7 @@ files:
83
83
  - ext/yrb_lite/Cargo.toml
84
84
  - ext/yrb_lite/extconf.rb
85
85
  - ext/yrb_lite/src/lib.rs
86
+ - ext/yrb_lite/src/protocol.rs
86
87
  - lib/yrb-lite.rb
87
88
  - lib/yrb_lite.rb
88
89
  - lib/yrb_lite/version.rb