yrby 0.6.0 → 0.7.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -28
  3. data/Cargo.lock +29 -0
  4. data/README.md +352 -103
  5. data/ext/yrby/Cargo.toml +3 -0
  6. data/ext/yrby/crates/html-core/Cargo.toml +15 -0
  7. data/ext/yrby/{src/render_rules.rs → crates/html-core/src/lib.rs} +6 -0
  8. data/ext/yrby/crates/lexical-html/Cargo.toml +16 -0
  9. data/ext/yrby/{src/lexical_html.rs → crates/lexical-html/src/lib.rs} +14 -10
  10. data/ext/yrby/crates/prosemirror-html/Cargo.toml +16 -0
  11. data/ext/yrby/{src/prosemirror_html.rs → crates/prosemirror-html/src/lib.rs} +14 -10
  12. data/ext/yrby/src/lib.rs +10 -10
  13. data/ext/yrby/src/protocol.rs +63 -0
  14. data/ext/yrby/src/read.rs +3 -3
  15. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/common.rs +355 -0
  16. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/dynamic.rs +276 -0
  17. data/ext/yrby/target/debug/build/clang-sys-e3ae45bd384f74c3/out/macros.rs +49 -0
  18. data/ext/yrby/target/debug/build/rb-sys-4407948463231c4f/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
  19. data/ext/yrby/target/debug/build/rb-sys-dbeea42737529c2d/out/bindings-0.9.128-mri-arm64-darwin23-3.4.7.rs +8934 -0
  20. data/ext/yrby/target/debug/build/serde-58ea0ee887cc2602/out/private.rs +6 -0
  21. data/ext/yrby/target/debug/build/serde_core-41f407c21c1f205e/out/private.rs +5 -0
  22. data/ext/yrby/target/debug/build/thiserror-0f1416a82ff26f22/out/private.rs +5 -0
  23. data/lib/generators/yrby/install/install_generator.rb +44 -0
  24. data/lib/generators/yrby/install/templates/document_channel.rb +36 -0
  25. data/lib/generators/yrby/tables/tables_generator.rb +41 -0
  26. data/lib/generators/yrby/tables/templates/create_y_tables.rb +24 -0
  27. data/lib/y/decoder.rb +64 -0
  28. data/lib/y/lexxy.rb +25 -4
  29. data/lib/y/version.rb +1 -1
  30. data/lib/y.rb +1 -0
  31. metadata +21 -5
data/ext/yrby/Cargo.toml CHANGED
@@ -14,6 +14,9 @@ magnus = "0.8"
14
14
  rb-sys = "0.9"
15
15
  yrs = { version = "0.27", features = ["sync"] }
16
16
  serde_json = "1.0"
17
+ yrs-html-core = { path = "crates/html-core" }
18
+ yrs-lexical-html = { path = "crates/lexical-html" }
19
+ yrs-prosemirror-html = { path = "crates/prosemirror-html" }
17
20
 
18
21
  [dev-dependencies]
19
22
  rb-sys-env = "0.1"
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "yrs-html-core"
3
+ description = "Internal core for the yrs-*-html renderer crates; depend on those instead"
4
+ repository = "https://github.com/jpcamara/yrby"
5
+ readme = "README.md"
6
+ version = "0.1.0"
7
+ edition = "2024"
8
+ rust-version = "1.85"
9
+ authors = ["JP Camara <johnpcamara@gmail.com>"]
10
+ license = "MIT"
11
+ publish = false
12
+
13
+ [dependencies]
14
+ yrs = { version = "0.27", features = ["sync"] }
15
+ serde_json = "1.0"
@@ -45,6 +45,12 @@ pub struct Emitter {
45
45
  frames: Vec<Vec<Segment>>,
46
46
  }
47
47
 
48
+ impl Default for Emitter {
49
+ fn default() -> Self {
50
+ Self::new()
51
+ }
52
+ }
53
+
48
54
  impl Emitter {
49
55
  pub fn new() -> Self {
50
56
  Emitter {
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "yrs-lexical-html"
3
+ description = "Render Lexical-shaped yrs documents to HTML, no browser or Node required"
4
+ repository = "https://github.com/jpcamara/yrby"
5
+ readme = "README.md"
6
+ version = "0.1.0"
7
+ edition = "2024"
8
+ rust-version = "1.85"
9
+ authors = ["JP Camara <johnpcamara@gmail.com>"]
10
+ license = "MIT"
11
+ publish = false
12
+
13
+ [dependencies]
14
+ yrs = { version = "0.27", features = ["sync"] }
15
+ serde_json = "1.0"
16
+ yrs-html-core = { path = "../html-core" }
@@ -39,15 +39,19 @@
39
39
  //! with the unwrapped span. Case-transform format bits are never rendered
40
40
  //! (their text-transform style is outside the sanitize whitelist).
41
41
  //!
42
- //! Custom nodes: rules are registered by `__type` (see `render_rules`) and
42
+ //! Custom nodes: rules are registered by `__type` (see `yrs-html-core`) and
43
43
  //! consulted before the built-in arms, so they extend the schema or override
44
44
  //! a built-in. Declarative rules render here; callback rules emit
45
45
  //! `Segment::Deferred` for the caller to fill in after the render.
46
46
 
47
- use crate::render_rules::{
48
- resolve_parts, xml_attrs_json, xml_ref_attr, Content, Emitter, NodeRule, Rules, Segment,
49
- TypeMap,
50
- };
47
+ // README examples are living code: compile-checked on every cargo test.
48
+ #[cfg(doctest)]
49
+ #[doc = include_str!("../README.md")]
50
+ mod readme_examples {}
51
+
52
+ // The full rules surface, re-exported: depend on this crate alone;
53
+ // yrs-html-core is an internal implementation crate.
54
+ pub use yrs_html_core::*;
51
55
  use yrs::types::text::YChange;
52
56
  use yrs::{
53
57
  Any, GetString, Map, Out, ReadTxn, Text, Xml, XmlElementRef, XmlFragment, XmlFragmentRef,
@@ -121,12 +125,12 @@ pub fn render_segments<T: ReadTxn>(
121
125
  Some(em.into_segments())
122
126
  }
123
127
 
124
- /// Rule-free rendering to a plain string — the fixture-parity surface most
125
- /// tests pin. With no callback rules, segments always flatten.
126
- #[cfg(test)]
128
+ /// Rule-free rendering to a plain string — the simplest way to use this
129
+ /// crate standalone, and the fixture-parity surface the tests pin. With no
130
+ /// callback rules, segments always flatten.
127
131
  pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
128
132
  render_segments(txn, fragment, &Rules::empty()).map(|segs| {
129
- crate::render_rules::flatten(segs)
133
+ yrs_html_core::flatten(segs)
130
134
  .into_html()
131
135
  .expect("no callback rules registered")
132
136
  })
@@ -1297,7 +1301,7 @@ mod tests {
1297
1301
  let txn = doc.transact();
1298
1302
  let frag = txn.get_xml_fragment("root").unwrap();
1299
1303
  let segs = render_segments(&txn, &frag, &rules).unwrap();
1300
- let html = crate::render_rules::flatten(segs).into_html().unwrap();
1304
+ let html = yrs_html_core::flatten(segs).into_html().unwrap();
1301
1305
  assert_eq!(
1302
1306
  html,
1303
1307
  "<p><a class=\"app-link\" href=\"https://x.example\">site</a>\
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "yrs-prosemirror-html"
3
+ description = "Render ProseMirror-shaped yrs documents to HTML, no browser or Node required"
4
+ repository = "https://github.com/jpcamara/yrby"
5
+ readme = "README.md"
6
+ version = "0.1.0"
7
+ edition = "2024"
8
+ rust-version = "1.85"
9
+ authors = ["JP Camara <johnpcamara@gmail.com>"]
10
+ license = "MIT"
11
+ publish = false
12
+
13
+ [dependencies]
14
+ yrs = { version = "0.27", features = ["sync"] }
15
+ serde_json = "1.0"
16
+ yrs-html-core = { path = "../html-core" }
@@ -36,10 +36,14 @@
36
36
  //! Declarative rules render here; callback rules emit `Segment::Deferred` for
37
37
  //! the caller to fill in after the render.
38
38
 
39
- use crate::render_rules::{
40
- any_attr_string, resolve_parts, xml_attrs_json, xml_ref_attr, Content, Emitter, MarkRule,
41
- NodeRule, Rules, Segment, TypeMap,
42
- };
39
+ // README examples are living code: compile-checked on every cargo test.
40
+ #[cfg(doctest)]
41
+ #[doc = include_str!("../README.md")]
42
+ mod readme_examples {}
43
+
44
+ // The full rules surface, re-exported: depend on this crate alone;
45
+ // yrs-html-core is an internal implementation crate.
46
+ pub use yrs_html_core::*;
43
47
  use yrs::types::text::YChange;
44
48
  use yrs::types::Attrs;
45
49
  use yrs::{
@@ -96,12 +100,12 @@ pub fn render_segments<T: ReadTxn>(
96
100
  Some(em.into_segments())
97
101
  }
98
102
 
99
- /// Rule-free rendering to a plain string — the fixture-parity surface most
100
- /// tests pin. With no callback rules, segments always flatten.
101
- #[cfg(test)]
103
+ /// Rule-free rendering to a plain string — the simplest way to use this
104
+ /// crate standalone, and the fixture-parity surface the tests pin. With no
105
+ /// callback rules, segments always flatten.
102
106
  pub fn render<T: ReadTxn>(txn: &T, fragment: &XmlFragmentRef) -> Option<String> {
103
107
  render_segments(txn, fragment, &Rules::empty()).map(|segs| {
104
- crate::render_rules::flatten(segs)
108
+ yrs_html_core::flatten(segs)
105
109
  .into_html()
106
110
  .expect("no callback rules registered")
107
111
  })
@@ -1232,7 +1236,7 @@ mod tests {
1232
1236
  let txn = doc.transact();
1233
1237
  let segs = render_segments(&txn, &frag, &rules).unwrap();
1234
1238
  assert_eq!(
1235
- crate::render_rules::flatten(segs).into_html().unwrap(),
1239
+ yrs_html_core::flatten(segs).into_html().unwrap(),
1236
1240
  "<aside class=\"callout callout--warning\"><div class=\"para\">careful</div></aside>"
1237
1241
  );
1238
1242
  }
@@ -1290,7 +1294,7 @@ mod tests {
1290
1294
  }
1291
1295
  let txn = doc.transact();
1292
1296
  let map = collect_node_types(&txn, &frag).unwrap();
1293
- let json = crate::render_rules::type_map_json(&map, |ty| {
1297
+ let json = yrs_html_core::type_map_json(&map, |ty| {
1294
1298
  if is_builtin(ty) {
1295
1299
  Some("builtin")
1296
1300
  } else {
data/ext/yrby/src/lib.rs CHANGED
@@ -7,16 +7,16 @@ use yrs::updates::decoder::Decode;
7
7
  use yrs::updates::encoder::Encode;
8
8
  use yrs::{Doc, GetString, ReadTxn, Transact};
9
9
 
10
- mod lexical_html;
11
- mod prosemirror_html;
12
10
  mod protocol;
13
11
  mod read;
14
- mod render_rules;
15
12
  use protocol::{
16
13
  classify_message, has_pending, integrated_update, merged_doc_update, update_advances_doc,
17
14
  update_is_ready,
18
15
  };
19
16
  use render_rules::{Rules, Segment};
17
+ pub(crate) use yrs_html_core as render_rules;
18
+ use yrs_lexical_html as lexical_html;
19
+ use yrs_prosemirror_html as prosemirror_html;
20
20
 
21
21
  /// Wrapper around yrs Doc.
22
22
  ///
@@ -297,13 +297,13 @@ impl RbDoc {
297
297
  match msg {
298
298
  Message::Sync(sync_msg) => match sync_msg {
299
299
  SyncMessage::SyncStep1(sv) => {
300
- // Respond with SyncStep2 carrying only *integrated*
301
- // state. Never hand a peer un-integrable pending
302
- // structs: the peer would park the same pending
303
- // forever and the state-vector/content mismatch drives
304
- // endless resync traffic. (integrated_update is a no-op
305
- // fast path when nothing is pending.)
306
- let update = integrated_update(doc, &sv)?;
300
+ // Respond with SyncStep2 carrying the doc's full
301
+ // state, pending included, matching Y.js's
302
+ // encodeStateAsUpdate. A peer parks a pending
303
+ // struct exactly as this doc does and heals it
304
+ // when the missing dependency arrives.
305
+ let txn = doc.transact();
306
+ let update = txn.encode_state_as_update_v1(&sv);
307
307
  let response = Message::Sync(SyncMessage::SyncStep2(update));
308
308
  Ok((0, 0, response.encode_v1()))
309
309
  }
@@ -135,6 +135,27 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
135
135
  let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
136
136
  let has_deletes = !update.delete_set().is_empty();
137
137
 
138
+ // A merged update can hide an internal gap behind a Skip block, and
139
+ // blocks past the gap can still integrate (yrs plants a Skip hole in the
140
+ // store) when their origins don't need the missing range. That moves
141
+ // neither the doc's public state vector (yrs caps it at the first hole)
142
+ // nor pending: invisible to every comparison below, so genuinely novel
143
+ // content would report "doesn't advance": applied, acked, but never
144
+ // recorded or broadcast. The update's own state_vector() caps at its
145
+ // first Skip the same way, so any insertion past a non-zero cap reveals
146
+ // such a gap: conservatively report it as advancing (record it; a
147
+ // duplicate in the log is harmless, dropped content is not). Standard
148
+ // Yjs providers never emit these frames (a client's own updates and
149
+ // diffs are gap-free), so this guards crafted or pathologically merged
150
+ // input, not a hot path.
151
+ let capped = update.state_vector();
152
+ for (client, ranges) in update.insertions(true).iter() {
153
+ let cap = capped.get(client);
154
+ if cap > 0 && ranges.iter().any(|range| range.end > cap) {
155
+ return Ok(true);
156
+ }
157
+ }
158
+
138
159
  // Fast path: blocks beyond the doc's state vector are content the doc
139
160
  // lacks — the update advances, no probe needed. The common case (a novel
140
161
  // edit) exits here; only retries and ambiguous diffs pay for the probe.
@@ -663,6 +684,48 @@ mod tests {
663
684
  );
664
685
  }
665
686
 
687
+ #[test]
688
+ fn a_gappy_update_with_novel_post_hole_content_advances() {
689
+ // A merged update hiding an internal gap behind a Skip, whose
690
+ // post-hole blocks integrate anyway (their origins don't need the
691
+ // missing range). Integration plants a Skip hole, so neither the
692
+ // public state vector nor pending moves, before the hidden-gap
693
+ // check, the probe comparison reported genuinely novel content as
694
+ // "doesn't advance": update_ready? accepted it, the doc applied it,
695
+ // but it was never recorded or broadcast. Unreachable through
696
+ // standard Yjs providers (a client's own updates and diffs are
697
+ // gap-free); this is a crafted/hostile frame shape.
698
+ let a = Doc::new();
699
+ let t1 = a.get_or_insert_text("one");
700
+ t1.insert(&mut a.transact_mut(), 0, "first");
701
+ let u1 = a
702
+ .transact()
703
+ .encode_state_as_update_v1(&yrs::StateVector::default());
704
+ t1.insert(&mut a.transact_mut(), 5, " second"); // the future hole
705
+ let sv2 = a.transact().state_vector();
706
+ // Novel content on a DIFFERENT root: no dependency on the hole.
707
+ let t3 = a.get_or_insert_text("two");
708
+ t3.insert(&mut a.transact_mut(), 0, "third");
709
+ let u3 = a.transact().encode_state_as_update_v1(&sv2);
710
+
711
+ let server = Doc::new();
712
+ server
713
+ .transact_mut()
714
+ .apply_update(yrs::Update::decode_v1(&u1).unwrap())
715
+ .unwrap();
716
+ // The crafted frame: u1 + u3 merged, u2's range an internal Skip.
717
+ let gappy = yrs::merge_updates_v1([&u1, &u3]).unwrap();
718
+
719
+ assert!(
720
+ update_is_ready(&server, &gappy).unwrap(),
721
+ "sanity: the sync flow would accept and apply this frame"
722
+ );
723
+ assert!(
724
+ update_advances_doc(&server, &gappy).unwrap(),
725
+ "novel content hiding behind a Skip must be recorded, not dropped"
726
+ );
727
+ }
728
+
666
729
  // Build a causal gap: `first` inserts "a", `dependent` inserts "b" after it,
667
730
  // so `dependent` alone parks as pending on a doc that lacks `first`.
668
731
  fn gap_pair() -> (Vec<u8>, Vec<u8>) {
data/ext/yrby/src/read.rs CHANGED
@@ -352,7 +352,7 @@ mod tests {
352
352
  // lexxy-realtime's test server and saving GET /content/:room.
353
353
  use yrs::updates::decoder::Decode;
354
354
  use yrs::Update;
355
- let bytes = include_bytes!("fixtures/lexical_linebreak.bin");
355
+ let bytes = include_bytes!("../crates/lexical-html/src/fixtures/lexical_linebreak.bin");
356
356
  let doc = Doc::new();
357
357
  doc.transact_mut()
358
358
  .apply_update(Update::decode_v1(bytes).unwrap())
@@ -369,7 +369,7 @@ mod tests {
369
369
  // upload's caption as its own line — while the divider stays silent.
370
370
  use yrs::updates::decoder::Decode;
371
371
  use yrs::{Transact, Update};
372
- let bytes = include_bytes!("fixtures/lexxy_full.bin");
372
+ let bytes = include_bytes!("../crates/lexical-html/src/fixtures/lexxy_full.bin");
373
373
  let doc = Doc::new();
374
374
  doc.transact_mut()
375
375
  .apply_update(Update::decode_v1(bytes).unwrap())
@@ -408,7 +408,7 @@ mod tests {
408
408
  // must come through (get_string alone dropped all the nested ones).
409
409
  use yrs::updates::decoder::Decode;
410
410
  use yrs::{Transact, Update};
411
- let bytes = include_bytes!("fixtures/lexical_rich.bin");
411
+ let bytes = include_bytes!("../crates/lexical-html/src/fixtures/lexical_rich.bin");
412
412
  let doc = Doc::new();
413
413
  {
414
414
  let mut txn = doc.transact_mut();