yrby 0.6.0-aarch64-linux → 0.6.1-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8ac7194fe7f383fbea760aae137ae6848f6291687eff4a334dc318d1600f467
4
- data.tar.gz: 98197e70d3928419eac8a590b50bff1a336ae53c62f2eda17d2b9147e1280cb0
3
+ metadata.gz: b673ec3d72751c36ecb0b8edc586c15f393e1136d5a1589f52bc3199bc872030
4
+ data.tar.gz: 1ff196e6cedb72da559c3c61a20cd54254c43ef08203a4d476819caf46af5384
5
5
  SHA512:
6
- metadata.gz: 9aa048cce5443d92b7192174da78525019535142323033b85ffe6788258097a9ccbca492c567d8b456491a130f067605b521e83b3b3f1e7858785d268b7158af
7
- data.tar.gz: fbaa5ffe1493344b10c012d9ec4284f85dc9734a7386a438ae40672733c48f0082d3c9d0d5e1556881597f08e10add4e360ffc40700c6864789320c4c22e5f28
6
+ metadata.gz: 81949023eb7db2147856515dadb883919a95fac2bb85bd3bc774ba64b37e1065cea942608a554f4a390cbb700a2df2992a378d98a039147329e6f58c1232201c
7
+ data.tar.gz: 5055fb7f8f94d381cb7ad1a12ed204059064a6dbced1ce1693e574491feb1a66d1a1fc124ca01b890af14f04a01ed04a35c5e0dedb03ab073c2c165da9697d5a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims
5
5
  to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.1] - 2026-08-04
8
+
9
+ ### Fixed
10
+
11
+ - `Y::Lexxy` emits the attachment tag the node was created with instead of
12
+ a hardcoded `<action-text-attachment>`. Lexxy makes the tag configurable
13
+ (`Lexxy.configure`'s `attachmentTagName`, paired with
14
+ `ActionText::Attachment.tag_name` in Rails), and each attachment node
15
+ stores its tag, so a custom-tag app's rendered HTML now matches its
16
+ editor. A stored value that doesn't look like a tag name falls back to
17
+ the default; documents from before the tag was stored render unchanged.
18
+
7
19
  ## [0.6.0] - 2026-07-11
8
20
 
9
21
  ### Added
data/README.md CHANGED
@@ -5,14 +5,16 @@
5
5
  Collaborative editing for Rails, backed by [y-crdt](https://github.com/y-crdt/y-crdt)
6
6
  (the Rust library behind Y.js). Your Rails server speaks the y-websocket sync
7
7
  protocol directly, so there's no separate Node process hosting the Y.js
8
- documents.
8
+ documents. Pronounced "yer-bee".
9
+
10
+ ![Two people typing on separate lines of the same document, each keystroke synced through a Rails server, seen from a third browser with labeled carets](docs/images/collab.gif)
9
11
 
10
12
  ```ruby
11
13
  class DocumentChannel < ApplicationCable::Channel
12
- include Y::ActionCable::Sync
14
+ include Y::ActionCable
13
15
 
14
- on_load { |key| MyStore.load(key) }
15
- on_change { |key, update| MyStore.append(key, update) }
16
+ on_load { |key| Y::Document.load_state(key) }
17
+ on_change { |key, update| Y::Document.append(key, update) }
16
18
 
17
19
  def subscribed = sync_subscribed(params[:id])
18
20
  def receive(data) = sync_receive(data, params[:id])
@@ -59,7 +61,7 @@ and `Doc#read_map` reconstruct it server-side, in Ruby.
59
61
  The surface is intentionally small, but the focus is durability, resiliency, delivery
60
62
  guarantees, correctness, and thread safety.
61
63
 
62
- Towards that goal, `yrby` adds capabilities that stand out even in the Yjs ecosystem:
64
+ Towards that goal, `yrby` adds opinionated defaults on top of normal Yjs syncing:
63
65
 
64
66
  - Built-in update acknowledgement: the `ActionCableProvider` in `yrby-client` will continue to
65
67
  send updates until an ack is received from the server. [`yrby-actioncable`](https://rubygems.org/gems/yrby-actioncable)
@@ -101,8 +103,9 @@ Issues and PRs are welcome.
101
103
  # Core CRDT + protocol primitives:
102
104
  gem "yrby"
103
105
 
104
- # For the Rails/ActionCable server concern (Y::ActionCable::Sync):
105
- gem "yrby-actioncable"
106
+ # For the Rails side (the sync channel, document models, the generator).
107
+ # Formerly yrby-actioncable; that name stops at 0.3.1.
108
+ gem "yrby-rails"
106
109
  ```
107
110
 
108
111
  Requires Ruby 3.4 or newer. The release workflow builds precompiled gems for
@@ -457,25 +460,72 @@ Y.wrap_update(update_bytes) # => wrap a raw doc update as a sync Update frame
457
460
 
458
461
  ### ActionCable Integration
459
462
 
460
- `Y::ActionCable::Sync` (from the `yrby-actioncable` gem) is a channel
461
- concern that implements the full y-websocket protocol (document sync +
462
- awareness/presence) over ActionCable:
463
+ In a Rails app, one generator creates the channel and the migration:
464
+
465
+ ```bash
466
+ bin/rails generate yrby:install
467
+ bin/rails db:migrate
468
+ ```
469
+
470
+ The models ship in the gem, the way Action Text owns
471
+ `ActionText::RichText`:
472
+
473
+ - **`Y::Document`** — one row per document, addressed two ways: by `key`
474
+ (what a channel addresses — one opaque, unique string, sometimes
475
+ app-supplied, never parsed) and, optionally, by polymorphic `record` +
476
+ `name` (which model attribute it backs; `name` is the attribute name,
477
+ `"body"` — one document per attribute per record, the
478
+ ActionText::RichText scheme). Key-only documents leave the binding nil.
479
+ Either side can arrive first: `Y::Document.for(record, name)` finds or
480
+ creates the binding, derives a readable key (`post/1/body`), and adopts
481
+ a key-only row already holding that key, so a channel writing first and
482
+ a binding created later converge on one document. The row also holds
483
+ the merged `state` snapshot — CRDT state only; derived data (rendered
484
+ HTML, search text) is the application's job, typically in the channel's
485
+ on_change. `.load_state(key)` / `.append(key, update)` are the store
486
+ calls the generated channel uses.
487
+ - **`Y::DocumentUpdate`** — the uncompacted tail: one delta per row,
488
+ compacted into `state` and deleted once the tail reaches `compact_every`
489
+ (default 64). Loading reads the snapshot plus the current tail; an
490
+ empty tail returns `state` directly. Compaction serializes on a
491
+ per-document row lock and skips causally-gapped rows — they're
492
+ quarantined until they heal rather than compacted into state or
493
+ deleted. Destroying a document deletes its updates with it.
494
+
495
+ The migration creates `y_documents` and `y_document_updates`. To rename
496
+ them, edit the generated migration and point `Y::Document.table_name` /
497
+ `Y::DocumentUpdate.table_name` at the new names in an initializer.
498
+
499
+ Storage is swappable: the channel only needs `on_load` and `on_change`
500
+ answered, and they can point at anything.
501
+
502
+ `include Y::ActionCable` (from the `yrby-rails` gem) is the channel
503
+ integration: the y-websocket protocol (document sync +
504
+ awareness/presence) over ActionCable. (`include Y::ActionCable::Sync`
505
+ keeps working and has the same effect.)
463
506
 
464
507
  ```ruby
465
508
  # app/channels/document_channel.rb
466
509
  class DocumentChannel < ApplicationCable::Channel
467
- include Y::ActionCable::Sync
510
+ include Y::ActionCable
468
511
 
469
- on_load { |key| MyStore.load(key) } # source of truth
470
- on_change { |key, update| MyStore.append(key, update) } # durable record
512
+ on_load { |key| Y::Document.load_state(key) } # rebuild from storage
513
+ on_change { |key, update| Y::Document.append(key, update) } # record, then broadcast
471
514
 
472
515
  def subscribed
516
+ return reject unless authorized?(params[:id])
517
+
473
518
  sync_subscribed params[:id]
474
519
  end
475
520
 
476
521
  def receive(data)
477
522
  sync_receive(data, params[:id])
478
523
  end
524
+
525
+ private
526
+
527
+ # Everyone is denied until you wire this to your app's auth.
528
+ def authorized?(_document_key) = false
479
529
  end
480
530
  ```
481
531
 
@@ -571,7 +621,7 @@ It is up to you to durably record it:
571
621
 
572
622
  ```ruby
573
623
  class DocumentChannel < ApplicationCable::Channel
574
- include Y::ActionCable::Sync
624
+ include Y::ActionCable
575
625
 
576
626
  # ...
577
627
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "generators/yrby/tables/tables_generator"
5
+
6
+ module Yrby
7
+ module Generators
8
+ # `bin/rails generate yrby:install` — a DocumentChannel speaking the
9
+ # y-websocket protocol over the gem's document storage, plus the storage
10
+ # migration (via yrby:tables). The models ship in the gem; only the
11
+ # migration lands in the app.
12
+ class InstallGenerator < ::Rails::Generators::Base
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ def create_channel
16
+ template "document_channel.rb", "app/channels/document_channel.rb"
17
+ end
18
+
19
+ def create_tables
20
+ invoke "yrby:tables"
21
+ end
22
+
23
+ def show_next_steps
24
+ say <<~NEXT
25
+
26
+ Next steps:
27
+
28
+ 1. Authorize document access: implement `authorized?` in
29
+ app/channels/document_channel.rb (it denies everyone until you do).
30
+ 2. bin/rails db:migrate
31
+ 3. Install the yrby-client npm package and connect an editor:
32
+
33
+ import { ActionCableProvider } from "yrby-client"
34
+ const provider = new ActionCableProvider(doc, consumer,
35
+ "DocumentChannel", { id: documentId })
36
+ provider.connect()
37
+
38
+ The README's Editors section links working integrations for
39
+ Tiptap, Lexxy, Rhino Editor, and CodeMirror.
40
+ NEXT
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collaborative documents over Action Cable: one channel speaking the
4
+ # y-websocket protocol (sync plus presence). Storage is Y::Document +
5
+ # Y::DocumentUpdate; a document is created on its first change and its
6
+ # history goes with it when it is destroyed. Point on_load/on_change
7
+ # elsewhere to swap storage.
8
+ class DocumentChannel < ApplicationCable::Channel
9
+ include Y::ActionCable
10
+
11
+ # Rebuild a document from durable storage (nil means a brand-new document).
12
+ on_load { |key| Y::Document.load_state(key) }
13
+
14
+ # Record each CRDT delta durably. Runs before the change is acknowledged
15
+ # or broadcast; if this raises, the change is neither acked nor relayed,
16
+ # and yrby-client retries it.
17
+ on_change { |key, update| Y::Document.append(key, update) }
18
+
19
+ def subscribed
20
+ return reject unless authorized?(params[:id])
21
+
22
+ sync_subscribed(params[:id])
23
+ end
24
+
25
+ def receive(data) = sync_receive(data, params[:id])
26
+
27
+ private
28
+
29
+ # Everyone is denied until you fill this in. Wire it to your app's auth:
30
+ # identify current_user on the cable connection, then check they may read
31
+ # and write this document. Don't lean on on_change raising for access
32
+ # control — that path exists for store failures.
33
+ def authorized?(_document_key)
34
+ false
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Yrby
7
+ module Generators
8
+ # `bin/rails generate yrby:tables` — the migration for the gem-owned
9
+ # document models (Y::Document + Y::DocumentUpdate). Invoked by
10
+ # yrby:install, and by other gems building on the same storage.
11
+ #
12
+ # Template notes (kept here, not in the emitted migration): state is
13
+ # 4.gigabytes - 1 (longblob on MySQL — a compacted snapshot is the whole
14
+ # document; a 16 MB cap would break compaction) and payload is
15
+ # 16.megabytes - 1 (one update can carry a big paste or a client's
16
+ # accumulated offline edits — the 64 KB default blob is too small).
17
+ # The partial unique index's WHERE only keeps
18
+ # key-only rows out of the index: uniqueness holds without it, since
19
+ # unique indexes treat NULLs as distinct on every supported database,
20
+ # and MySQL drops the predicate harmlessly. y_document_updates indexes
21
+ # (document_id, pending) instead of bare document_id: the prefix
22
+ # serves the tail and foreign-key lookups, and the pair serves the
23
+ # clean-row count every append runs.
24
+ class TablesGenerator < ::Rails::Generators::Base
25
+ include ActiveRecord::Generators::Migration
26
+
27
+ source_root File.expand_path("templates", __dir__)
28
+
29
+ def create_migration_file
30
+ migration_template "create_y_tables.rb",
31
+ File.join(db_migrate_path, "create_y_tables.rb")
32
+ end
33
+
34
+ private
35
+
36
+ def migration_version
37
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateYTables < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :y_documents do |t|
6
+ t.string :key, null: false, index: { unique: true }
7
+ t.references :record, polymorphic: true, null: true, index: false
8
+ t.string :name
9
+ t.binary :state, limit: 4.gigabytes - 1
10
+ t.timestamps
11
+ t.index %i[record_type record_id name], unique: true,
12
+ where: "record_type IS NOT NULL",
13
+ name: "index_y_documents_on_record_and_name"
14
+ end
15
+
16
+ create_table :y_document_updates do |t|
17
+ t.references :document, null: false, foreign_key: { to_table: :y_documents }, index: false
18
+ t.binary :payload, null: false, limit: 16.megabytes - 1
19
+ t.boolean :pending, null: false, default: false
20
+ t.datetime :created_at, null: false
21
+ t.index %i[document_id pending]
22
+ end
23
+ end
24
+ end
data/lib/y/3.4/yrby.so CHANGED
Binary file
data/lib/y/4.0/yrby.so CHANGED
Binary file
data/lib/y/lexxy.rb CHANGED
@@ -52,24 +52,44 @@ module Y
52
52
  # and presence mirror Lexxy's exportDOM (nulls omitted, `previewable`
53
53
  # only when true, `presentation="gallery"` always).
54
54
  def self.upload(node)
55
- out = +"<action-text-attachment"
55
+ tag = attachment_tag(node)
56
+ out = "<#{tag}"
56
57
  out << attachment_attr(node, "sgid", "sgid")
57
58
  out << %( previewable="true") if node.attrs["previewable"] == true
58
59
  [%w[url src], %w[alt altText], %w[caption caption],
59
60
  %w[content-type contentType], %w[filename fileName],
60
61
  %w[filesize fileSize], %w[width width], %w[height height]]
61
62
  .each { |html_name, stored| out << attachment_attr(node, html_name, stored) }
62
- %(#{out} presentation="gallery"></action-text-attachment>)
63
+ %(#{out} presentation="gallery"></#{tag}>)
63
64
  end
64
65
 
65
66
  # A content attachment (mention, embed): `content` carries the escaped
66
67
  # inner HTML; `plainText` is not exported.
67
68
  def self.mention(node)
68
- out = +"<action-text-attachment"
69
+ tag = attachment_tag(node)
70
+ out = "<#{tag}"
69
71
  out << attachment_attr(node, "sgid", "sgid")
70
72
  out << attachment_attr(node, "content", "innerHtml")
71
73
  out << attachment_attr(node, "content-type", "contentType")
72
- "#{out}></action-text-attachment>"
74
+ "#{out}></#{tag}>"
75
+ end
76
+
77
+ # Lexxy's attachment tag is configurable (`Lexxy.configure`'s
78
+ # attachmentTagName, paired with ActionText::Attachment.tag_name on the
79
+ # Rails side), and each attachment node stores the tag it was created
80
+ # with. Emit the stored tag. The value is stored document data, not
81
+ # markup, so anything that doesn't look like a tag name falls back to
82
+ # ActionText's default.
83
+ def self.attachment_tag(node)
84
+ tag = node.attrs["tagName"].to_s
85
+ tag.match?(/\A[a-zA-Z][a-zA-Z0-9-]*\z/) ? tag : "action-text-attachment"
86
+ end
87
+
88
+ # An in-flight upload placeholder. Only the uploader's browser can
89
+ # complete it, so it is never finished content -- materialized output
90
+ # (Action Text bodies, search text) must not carry it.
91
+ def self.pending_upload(_node)
92
+ ""
73
93
  end
74
94
 
75
95
  # A stored nil (unset) is skipped; a stored empty string still emits.
@@ -90,6 +110,7 @@ module Y
90
110
  "tablecell" => { contains: :blocks, render: method(:table_cell) },
91
111
  "listitem" => { contains: :blocks, render: method(:list_item) },
92
112
  "action_text_attachment" => method(:upload),
113
+ "action_text_attachment_upload" => method(:pending_upload),
93
114
  "custom_action_text_attachment" => method(:mention)
94
115
  }.freeze
95
116
 
data/lib/y/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Y
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - JP Camara
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-12 00:00:00.000000000 Z
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -55,7 +55,7 @@ dependencies:
55
55
  description: 'yrby is a thread-safe Ruby binding over the Rust y-crdt (yrs) library:
56
56
  CRDT documents, awareness/presence, and the y-websocket sync protocol primitives,
57
57
  with the GVL released during native work so documents sync in parallel. The ActionCable/Rails
58
- integration lives in the companion yrby-actioncable gem.'
58
+ integration lives in the companion yrby-rails gem.'
59
59
  email:
60
60
  - johnpcamara@gmail.com
61
61
  executables: []
@@ -65,6 +65,10 @@ files:
65
65
  - CHANGELOG.md
66
66
  - LICENSE
67
67
  - README.md
68
+ - lib/generators/yrby/install/install_generator.rb
69
+ - lib/generators/yrby/install/templates/document_channel.rb
70
+ - lib/generators/yrby/tables/tables_generator.rb
71
+ - lib/generators/yrby/tables/templates/create_y_tables.rb
68
72
  - lib/y.rb
69
73
  - lib/y/3.4/yrby.so
70
74
  - lib/y/4.0/yrby.so