yrby-rails 0.6.1 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG-rails.md +153 -0
- data/README.md +608 -384
- data/app/channels/y/document_channel.rb +127 -0
- data/app/models/y/document.rb +10 -8
- data/lib/generators/yrby/install/USAGE +8 -4
- data/lib/generators/yrby/install/install_generator.rb +24 -13
- data/lib/generators/yrby/install/templates/document_channel.rb +4 -7
- data/lib/generators/yrby/tables/tables_generator.rb +7 -4
- data/lib/generators/yrby/tables/templates/create_y_tables.rb +1 -1
- data/lib/y/action_cable/sync.rb +101 -15
- data/lib/y/action_cable.rb +11 -0
- data/lib/y/collaborative/attribute.rb +89 -0
- data/lib/y/collaborative/helper.rb +51 -0
- data/lib/y/collaborative.rb +101 -0
- data/lib/yrby/engine.rb +9 -0
- data/lib/yrby/rails/version.rb +1 -1
- data/lib/yrby-rails.rb +1 -0
- metadata +35 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83a5440fa6532dbedcf992660959297611b9b7cf958f7c7c6c1522dd0ac162b0
|
|
4
|
+
data.tar.gz: 23509b37d1bb0ce706c648a779993236addb244cb603031415de4084a58e4636
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c51b2e0b9acefc89d6c69e3b935206d195ccaba7b302ec13316c39f7fab0115d09fd7a5d108f537294b8c069a54ef2e1a53fc19b39ec21873e7b61be866e7a5d
|
|
7
|
+
data.tar.gz: 5ffd94a472cd16ada77f62e57c7bd571f7df461a9552756212ef6fd12c9c1a2cb92bdb3ac72fe0320a1992d27f6b22ce8964240db81057ffaaa4cc2be1872124
|
data/CHANGELOG-rails.md
CHANGED
|
@@ -5,6 +5,159 @@ documented here. The
|
|
|
5
5
|
format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
6
|
this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.8.0] - 2026-11-03
|
|
11
|
+
|
|
12
|
+
Requires yrby 0.9.0 or later.
|
|
13
|
+
|
|
14
|
+
### Experimental
|
|
15
|
+
|
|
16
|
+
These are new, and they may change in a minor release. See
|
|
17
|
+
[docs/experimental.md](docs/experimental.md).
|
|
18
|
+
|
|
19
|
+
- `collaborative_document(:body).edit { |doc| ... }` edits a record's
|
|
20
|
+
document from Ruby. It records the change in the document's rows, encrypted
|
|
21
|
+
ones included, and broadcasts it to open editors.
|
|
22
|
+
`Y::ActionCable.broadcast(key, update)` sends an already recorded update.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `collaborative_document(name)` answers `grant(expires_in:)` and `channel`:
|
|
27
|
+
the grant a page or client subscribes with, and the channel that accepts
|
|
28
|
+
it. A gem with its own grants and channel, such as lexxy-realtime, returns
|
|
29
|
+
its own document with both.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `collaborative_document_tag` renders the document's grant and channel, so
|
|
34
|
+
it works for a gem that returns its own document. `channel:` still
|
|
35
|
+
overrides the channel.
|
|
36
|
+
|
|
37
|
+
### Deprecated
|
|
38
|
+
|
|
39
|
+
- `collaborative_sgid(name, expires_in:)`. Use
|
|
40
|
+
`collaborative_document(name).grant(expires_in:)`. It logs through
|
|
41
|
+
`Y.deprecator`, which the engine adds to `Rails.application.deprecators`,
|
|
42
|
+
and goes away in yrby-rails 0.9.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- The channel relays a client's SyncStep2 to peers as a plain update.
|
|
47
|
+
Relayed as is, a peer that was still joining could take it for the answer
|
|
48
|
+
to its own handshake and report itself loaded before it had the server's
|
|
49
|
+
state.
|
|
50
|
+
|
|
51
|
+
## [0.7.0] - 2026-10-01
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `Y::DocumentChannel.authorize_document { |record, name| ... }` runs the
|
|
56
|
+
application's permission check when a client subscribes, on top of the
|
|
57
|
+
signed grant. The block runs in channel context. If it denies access, the
|
|
58
|
+
channel rejects that subscription without opening a stream or serving
|
|
59
|
+
state, and other subscriptions on the connection keep working. Without a
|
|
60
|
+
block, a valid signed grant is enough.
|
|
61
|
+
|
|
62
|
+
The check runs once per subscription, so a permission you revoke
|
|
63
|
+
mid-session takes effect the next time that client subscribes.
|
|
64
|
+
- `record.collaborative_document(name)` returns a bound
|
|
65
|
+
`Y::Collaborative::Attribute`, which both application code and the shipped
|
|
66
|
+
channel use to read and write the document. `y_doc` builds a native
|
|
67
|
+
`Y::Doc`, and `load_state`, `append`, and `key` all use the same storage
|
|
68
|
+
class. `key` and `load_state` don't create a row, but the first `append`
|
|
69
|
+
does. Use `.document_row` for the built-in row operations.
|
|
70
|
+
- `Y::Document.key_for(record, name)` returns the conventional key without
|
|
71
|
+
creating a document row.
|
|
72
|
+
- `collaborative_sgid(name, expires_in:)` and
|
|
73
|
+
`collaborative_document_tag(record, name, expires_in:, refresh:)` let you
|
|
74
|
+
set the grant lifetime, which used to be GlobalID's default with no way to
|
|
75
|
+
shorten it. `refresh:` is a URL the element fetches when a subscription is
|
|
76
|
+
rejected. That action re-runs the app's authorization and renders
|
|
77
|
+
`{ grant: ... }`, and the client resubscribes the same session with the new
|
|
78
|
+
grant. The client fetches the URL once per rejection and does not poll.
|
|
79
|
+
|
|
80
|
+
- `Y::DocumentChannel` ships in the gem, so apps don't need to write a
|
|
81
|
+
channel. Clients subscribe with the signed grant the page rendered
|
|
82
|
+
(`{ grant:, name: }`). The channel finds the record from the grant, loads
|
|
83
|
+
the document, and stores changes in `Y::Document`. It rejects grants that
|
|
84
|
+
are missing, tampered with, for the wrong attribute, or for a destroyed
|
|
85
|
+
record.
|
|
86
|
+
|
|
87
|
+
- `collaborative_document_tag(record, name, **options)` is available in
|
|
88
|
+
Action View through the engine. It renders the mount element with the
|
|
89
|
+
signed grant, the attribute name, and the channel name as data attributes.
|
|
90
|
+
Anyone holding the grant can open the document, the same as with a
|
|
91
|
+
`turbo_stream_from` stream name, so only render the tag on pages where the
|
|
92
|
+
user is allowed to edit the record.
|
|
93
|
+
|
|
94
|
+
- Channels get `Y::Document` storage by default. Declare `on_load` and
|
|
95
|
+
`on_change` if you want a different store. Outside a yrby-rails app there
|
|
96
|
+
is still no default, and subscribing raises until both hooks are declared.
|
|
97
|
+
|
|
98
|
+
- `has_collaborative_document :name, encrypted: true` declares which storage
|
|
99
|
+
class backs an attribute, and `Y::DocumentChannel` uses that class. Every
|
|
100
|
+
load and append for an encrypted attribute goes through
|
|
101
|
+
`Y::EncryptedDocument`, so the bytes are ciphertext at rest and the plain
|
|
102
|
+
classes can't read them. The model decides whether an attribute is
|
|
103
|
+
encrypted, and a page or client can't change that. Undeclared attributes use
|
|
104
|
+
plain `Y::Document`.
|
|
105
|
+
|
|
106
|
+
- `Y::Collaborative` adds signed tokens for record-backed documents, and the
|
|
107
|
+
engine includes it into ActiveRecord::Base. A page creates a token with
|
|
108
|
+
`record.collaborative_sgid(:body)`, and the channel finds the record with
|
|
109
|
+
`Y::Collaborative.locate(params[:grant], :body)`. The token is a signed
|
|
110
|
+
GlobalID scoped to one attribute, so a token for one field can't open
|
|
111
|
+
another. This is the standard way to implement `authorized?` for
|
|
112
|
+
record-backed documents. lexxy-realtime already uses this flow, and now it
|
|
113
|
+
ships in yrby-rails.
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
|
|
117
|
+
- **Breaking:** channels reject every subscription until they define
|
|
118
|
+
`authorized?(key)`. `sync_subscribed` now calls it before opening a stream
|
|
119
|
+
or serving state, and the default returns `false`. Add the method to every
|
|
120
|
+
channel that includes `Y::ActionCable`:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def authorized?(key)
|
|
126
|
+
current_user&.can_edit?(key)
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Return `true` for public documents. If the default is what rejected a
|
|
131
|
+
subscription, the log message says so.
|
|
132
|
+
|
|
133
|
+
- `yrby:install` now creates only the storage migration, since the gem ships
|
|
134
|
+
`Y::DocumentChannel`. Pass `--channel` to also generate an application
|
|
135
|
+
channel for custom authorization or room-keyed documents. That channel
|
|
136
|
+
rejects every subscription until you implement `authorized?`.
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
|
|
140
|
+
- Document elements now attach editors to sessions that exist independently
|
|
141
|
+
of any editor, so pending edits survive navigation under their original
|
|
142
|
+
grant and rejected deliveries can still be recovered. Turbo previews are
|
|
143
|
+
inert, and cached HTML contains no CRDT bytes.
|
|
144
|
+
- When an attribute morphs, the element releases the old editor binding and
|
|
145
|
+
acquires the new document. It doesn't move pending edits or authorization
|
|
146
|
+
over to the new document.
|
|
147
|
+
|
|
148
|
+
- Default storage now supplies the loader and recorder together, and
|
|
149
|
+
declaring only one custom hook raises before subscribing or acknowledging
|
|
150
|
+
an update. Previously, reads and writes silently went to different stores.
|
|
151
|
+
|
|
152
|
+
- Signed grants work even when the host app doesn't load Active Job.
|
|
153
|
+
|
|
154
|
+
- The `yrby:tables` migration template caps `y_documents.state` at
|
|
155
|
+
`1.gigabyte - 1` instead of `4.gigabytes - 1`. Postgres raises
|
|
156
|
+
`ArgumentError` for binary limits above 1 GB - 1, so `db:migrate` on a
|
|
157
|
+
fresh Postgres app failed. MySQL maps both values to `longblob`, and
|
|
158
|
+
SQLite ignores limits, so nothing changes there. Apps that already
|
|
159
|
+
migrated are unaffected.
|
|
160
|
+
|
|
8
161
|
## [0.6.1] - 2026-08-11
|
|
9
162
|
|
|
10
163
|
### Fixed
|