yrb-lite 0.1.0.beta5-arm64-darwin → 0.1.0.beta6-arm64-darwin
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 +38 -16
- data/README.md +100 -146
- data/lib/yrb_lite/3.4/yrb_lite.bundle +0 -0
- data/lib/yrb_lite/4.0/yrb_lite.bundle +0 -0
- data/lib/yrb_lite/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f354eaf881986317d81b5f8db2f1847b6b789d03e4a3bb2036259be7db3c1c33
|
|
4
|
+
data.tar.gz: 9ce517fb9005275d4fc16759bdb985027038634c275f161ea5139d0479eb3146
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 200dde12e999dabc186526554706f3b7ae83b981fcbd435d056ea370c75904071b9bea02af497a7c6bb99be90ecd4669f6e5f0f2bd6b98e3f5cd20947b13b27d
|
|
7
|
+
data.tar.gz: fa765b55fcb83423be6c059c942654e5f152fbacbb1161d6e6c1ccfb32f05e010773c9e0295cb8ca42ccf036f8efa913017c4130e52b86f8720c37bda3a4527c
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.0.beta6] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
(yrb-lite core gem. The `yrb-lite-client` npm package ships these client changes as 0.1.2.)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `yrb-lite-client`, the TypeScript client package for the yrb-lite
|
|
16
|
+
ActionCable/AnyCable protocol. It provides `ActionCableProvider`,
|
|
17
|
+
`YProtocolSession`, and the standalone `ReliableSync` delivery core.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Document delivery is ack-tracked by default in `yrb-lite-client`: document
|
|
22
|
+
frames use `{ update, id }`, acknowledgements use `{ ack }`, and pending
|
|
23
|
+
document updates stay queued until acked.
|
|
24
|
+
- The ActionCable protocol surface uses a single canonical document envelope:
|
|
25
|
+
`{ "update" => "<base64 frame>" }`.
|
|
26
|
+
- AnyCable awareness/presence uses an awareness-only whisper envelope,
|
|
27
|
+
`{ awareness: "<base64 awareness frame>" }`, while document frames stay on
|
|
28
|
+
the server persistence/ack path.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Incoming protocol frames are validated before mutating documents or awareness
|
|
33
|
+
state, including trailing-byte rejection on the TypeScript client.
|
|
34
|
+
- Native/Rust protocol entry points reject wire client IDs that are unsafe for
|
|
35
|
+
JavaScript clients.
|
|
36
|
+
- `lib0` is declared as a direct runtime dependency of `yrb-lite-client`.
|
|
37
|
+
|
|
9
38
|
## [0.1.0.beta5] - 2026-06-18
|
|
10
39
|
|
|
11
40
|
### Changed
|
|
@@ -33,9 +62,7 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
33
62
|
`current_user`, `params`, request/connection-scoped accessors -- directly,
|
|
34
63
|
instead of plumbing them in through a thread-local. A non-Proc callable (an
|
|
35
64
|
object responding to `#call`) is still invoked with `#call` and its own
|
|
36
|
-
context.
|
|
37
|
-
document registry during a cold load or eviction, where no connection
|
|
38
|
-
instance exists, so they remain key-only. Existing block recorders that use
|
|
65
|
+
context. Existing block recorders that use
|
|
39
66
|
only the `(key, update)` arguments and lexically-scoped constants are
|
|
40
67
|
unaffected; the only behavioral change is `self` inside the block.
|
|
41
68
|
|
|
@@ -67,15 +94,13 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
67
94
|
|
|
68
95
|
- Reliable delivery (opt-in, client-driven). A client may tag a document update
|
|
69
96
|
with an `"id"`; the server replies `{ "ack": <id> }` once the update has been
|
|
70
|
-
|
|
97
|
+
durably recorded. This lets an
|
|
71
98
|
ack-aware client retain and retransmit an update until delivery is confirmed,
|
|
72
|
-
so an edit can't be silently lost on a flaky connection.
|
|
73
|
-
`"id"
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
causally-complete delta), plus a minimal reference client and an intensive
|
|
78
|
-
message-loss stress test.
|
|
99
|
+
so an edit can't be silently lost on a flaky connection. Clients that omit
|
|
100
|
+
`"id"` are still accepted, but their delivery is not ack-tracked.
|
|
101
|
+
- Demo coverage for reliable delivery with "sync-since-last-ack" framing (the
|
|
102
|
+
unacknowledged tail is sent as one merged, causally-complete delta), plus a
|
|
103
|
+
minimal reference client and an intensive message-loss stress test.
|
|
79
104
|
|
|
80
105
|
### Fixed
|
|
81
106
|
|
|
@@ -95,14 +120,11 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
95
120
|
native extension). The GVL is released during CRDT work so docs can run in
|
|
96
121
|
parallel on MRI.
|
|
97
122
|
- `YrbLite::Sync` ActionCable channel concern implementing the y-websocket
|
|
98
|
-
protocol (document sync plus awareness/presence).
|
|
99
|
-
the [`@y-rb/actioncable`](https://www.npmjs.com/package/@y-rb/actioncable)
|
|
100
|
-
browser provider, and accepts its `{ update: ... }` envelope and `{ m: ... }`.
|
|
123
|
+
protocol (document sync plus awareness/presence).
|
|
101
124
|
- A "record-before-distribute" mode via an `on_change` hook, so every change is
|
|
102
125
|
recorded durably before it's applied or relayed.
|
|
103
126
|
- Presence cleanup on disconnect, and idle-document eviction.
|
|
104
|
-
-
|
|
105
|
-
`sync_backend :store` (stateless, AnyCable-ready, multi-process).
|
|
127
|
+
- Store-backed ActionCable delivery for AnyCable and multi-process use.
|
|
106
128
|
- Hardening against bad input: malformed or multi-message frames are dropped
|
|
107
129
|
before processing or relay, and native panics are contained at the FFI
|
|
108
130
|
boundary.
|
data/README.md
CHANGED
|
@@ -9,26 +9,27 @@ documents.
|
|
|
9
9
|
|
|
10
10
|
```ruby
|
|
11
11
|
class DocumentChannel < ApplicationCable::Channel
|
|
12
|
-
include YrbLite::Sync
|
|
12
|
+
include YrbLite::ActionCable::Sync
|
|
13
13
|
|
|
14
14
|
def subscribed = sync_for(params[:id])
|
|
15
15
|
def receive(data) = sync_receive(data)
|
|
16
|
-
def unsubscribed =
|
|
16
|
+
def unsubscribed = sync_unsubscribed(params[:id])
|
|
17
17
|
end
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
On the browser, use the
|
|
21
|
-
|
|
20
|
+
On the browser, use the `yrb-lite-client` `ActionCableProvider`. Tiptap,
|
|
21
|
+
ProseMirror, and BlockNote all sync through the `Y.Doc` you pass in and the
|
|
22
|
+
provider's Awareness instance, unless you supply your own.
|
|
22
23
|
|
|
23
24
|
## What you get
|
|
24
25
|
|
|
25
|
-
- Thread-safe `Doc` and `Awareness`. You can share them
|
|
26
|
-
|
|
26
|
+
- Thread-safe Ruby wrappers for `Doc` and `Awareness`. You can share them
|
|
27
|
+
across Puma threads; native CRDT work runs with the GVL released.
|
|
27
28
|
- The y-websocket protocol (document sync plus awareness/presence) as a
|
|
28
29
|
one-include ActionCable concern.
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
goes out to anyone.
|
|
30
|
+
- Store-backed ActionCable/AnyCable delivery for multi-process deployments.
|
|
31
|
+
- Authoritative record-before-distribute semantics: each document change is
|
|
32
|
+
recorded durably before it goes out to anyone.
|
|
32
33
|
|
|
33
34
|
What it doesn't do: auth, read-only connections, rate limiting, webhooks,
|
|
34
35
|
metrics. Hocuspocus ships extensions for those; here you'd build them with
|
|
@@ -36,21 +37,26 @@ Rails.
|
|
|
36
37
|
|
|
37
38
|
## Testing
|
|
38
39
|
|
|
39
|
-
Ruby and Rust unit tests cover the core
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
The benchmark
|
|
43
|
-
welcome.
|
|
40
|
+
Ruby and Rust unit tests cover the core. CI also runs the npm client tests and a
|
|
41
|
+
Rails demo smoke slice against the real ActionCable stack. The demo includes
|
|
42
|
+
heavier local suites for hostile input, crash recovery, multi-browser editing,
|
|
43
|
+
AnyCable, and load testing. The benchmark note below is from a single laptop.
|
|
44
|
+
Issues and PRs are welcome.
|
|
44
45
|
|
|
45
46
|
## Install
|
|
46
47
|
|
|
47
48
|
```ruby
|
|
49
|
+
# Core CRDT + protocol primitives:
|
|
48
50
|
gem "yrb-lite"
|
|
51
|
+
|
|
52
|
+
# For the Rails/ActionCable server concern (YrbLite::ActionCable::Sync):
|
|
53
|
+
gem "yrb-lite-actioncable"
|
|
49
54
|
```
|
|
50
55
|
|
|
51
|
-
Requires Ruby 3.4 or newer.
|
|
52
|
-
3.4 and 4.0
|
|
53
|
-
|
|
56
|
+
Requires Ruby 3.4 or newer. The release workflow builds precompiled gems for
|
|
57
|
+
Ruby 3.4 and 4.0 across the supported Ruby platforms, with native smoke tests
|
|
58
|
+
on Linux x86_64 and macOS arm64. Installing from a matching platform gem needs
|
|
59
|
+
no Rust; a source build needs [Rust](https://rustup.rs).
|
|
54
60
|
|
|
55
61
|
To work on the gem itself:
|
|
56
62
|
|
|
@@ -128,44 +134,43 @@ send_to_peer(response) unless response.empty?
|
|
|
128
134
|
|
|
129
135
|
### ActionCable Integration
|
|
130
136
|
|
|
131
|
-
`YrbLite::Sync`
|
|
132
|
-
protocol (document sync +
|
|
137
|
+
`YrbLite::ActionCable::Sync` (from the `yrb-lite-actioncable` gem) is a channel
|
|
138
|
+
concern that implements the full y-websocket protocol (document sync +
|
|
139
|
+
awareness/presence) over ActionCable:
|
|
133
140
|
|
|
134
141
|
```ruby
|
|
135
142
|
# app/channels/document_channel.rb
|
|
136
143
|
class DocumentChannel < ApplicationCable::Channel
|
|
137
|
-
include YrbLite::Sync
|
|
144
|
+
include YrbLite::ActionCable::Sync
|
|
138
145
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
# on_save { |key, update| Document.find_by(key: key)&.update!(content: update) }
|
|
146
|
+
on_load { |key| MyStore.load(key) } # source of truth
|
|
147
|
+
on_change { |key, update| MyStore.append(key, update) } # durable record
|
|
142
148
|
|
|
143
149
|
def subscribed
|
|
144
150
|
sync_for params[:id]
|
|
145
151
|
end
|
|
146
152
|
|
|
147
153
|
def receive(data)
|
|
148
|
-
sync_receive(data)
|
|
154
|
+
sync_receive(data, params[:id])
|
|
149
155
|
end
|
|
150
156
|
|
|
151
157
|
def unsubscribed
|
|
152
|
-
|
|
158
|
+
sync_unsubscribed(params[:id])
|
|
153
159
|
end
|
|
154
160
|
end
|
|
155
161
|
```
|
|
156
162
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
The concern is store-backed. A handshake is answered from `on_load`; document
|
|
164
|
+
changes are checked against that durable state, recorded through `on_change`,
|
|
165
|
+
then broadcast. Nothing authoritative is kept in ActionCable process memory, so
|
|
166
|
+
AnyCable RPC workers, Puma workers, and separate dynos can all handle messages
|
|
167
|
+
for the same document as long as they share the same store and cable adapter.
|
|
162
168
|
|
|
163
|
-
`
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
it, the in-memory copy is the only one and stays put.
|
|
169
|
+
`on_load` and `on_change` are required. If either is missing, the channel fails
|
|
170
|
+
closed before it can acknowledge or broadcast edits. Presence is ephemeral:
|
|
171
|
+
awareness frames are relayed, and `yrb-lite-client` sends a best-effort
|
|
172
|
+
presence-removal frame on disconnect/pagehide, with the client-side awareness
|
|
173
|
+
timeout as the fallback for abrupt disconnects.
|
|
169
174
|
|
|
170
175
|
Incoming frames are validated as a single well-formed protocol message before
|
|
171
176
|
anything processes or relays them. Malformed, truncated, multi-message,
|
|
@@ -182,37 +187,19 @@ Broadcasts cross processes through the Action Cable adapter, so it needs to be a
|
|
|
182
187
|
real one (`redis` or `solid_cable`, not `async`). With that in place, a change
|
|
183
188
|
on one process reaches clients on all of them.
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
tells a process to skip its own.
|
|
190
|
-
|
|
191
|
-
A cold process (no copy yet) rebuilds from the durable store through `on_load`.
|
|
192
|
-
In authoritative mode the store is always current, since changes are recorded
|
|
193
|
-
before they go out. Record-before-distribute therefore holds across processes:
|
|
194
|
-
whichever process receives a change records it to the shared store before
|
|
195
|
-
anyone, anywhere, sees it.
|
|
190
|
+
Every process rebuilds document state from the durable store through `on_load`.
|
|
191
|
+
Because changes are recorded before broadcast, record-before-distribute holds
|
|
192
|
+
across processes: whichever process receives a change records it to the shared
|
|
193
|
+
store before anyone, anywhere, sees it.
|
|
196
194
|
|
|
197
195
|
`bun multiprocess.mjs` in the demo runs clients across two processes and checks
|
|
198
|
-
|
|
199
|
-
shared log.
|
|
200
|
-
|
|
201
|
-
##### AnyCable (`sync_backend :store`)
|
|
196
|
+
convergence, fresh reads on both, presence across processes, and one shared log.
|
|
202
197
|
|
|
203
|
-
|
|
204
|
-
block running in Ruby for each broadcast. AnyCable breaks both assumptions.
|
|
205
|
-
anycable-go delivers broadcasts outside Ruby, so the block never runs. Each RPC
|
|
206
|
-
gets a fresh channel instance, which means ivars set in `subscribed` are gone by
|
|
207
|
-
`receive`. And there's no fixed worker-to-document mapping to lean on.
|
|
208
|
-
|
|
209
|
-
`sync_backend :store` is the path for that: stateless per message, no warm
|
|
210
|
-
copy.
|
|
198
|
+
##### AnyCable
|
|
211
199
|
|
|
212
200
|
```ruby
|
|
213
201
|
class DocumentChannel < ApplicationCable::Channel
|
|
214
|
-
include YrbLite::Sync
|
|
215
|
-
sync_backend :store
|
|
202
|
+
include YrbLite::ActionCable::Sync
|
|
216
203
|
|
|
217
204
|
on_load { |key| MyStore.load(key) } # required: source of truth
|
|
218
205
|
on_change { |key, update| MyStore.append(key, update) } # required: record
|
|
@@ -227,6 +214,10 @@ end
|
|
|
227
214
|
- A handshake (SyncStep1) is answered from the store. Changes are recorded, then
|
|
228
215
|
broadcast. Nothing is held in Ruby between calls, so any worker can handle any
|
|
229
216
|
message.
|
|
217
|
+
- Document frames use the normal server path. Awareness/presence uses a
|
|
218
|
+
separate awareness stream with AnyCable `whisper: true`, so cursor traffic can
|
|
219
|
+
take the low-latency client-to-client path without bypassing document
|
|
220
|
+
durability.
|
|
230
221
|
- Pass `params[:id]` into `sync_receive`/`sync_unsubscribed` so the document key
|
|
231
222
|
survives AnyCable's per-command instances.
|
|
232
223
|
- The sender gets its own updates echoed back (no Ruby callback to filter them).
|
|
@@ -234,34 +225,31 @@ end
|
|
|
234
225
|
|
|
235
226
|
The demo checks this against a real anycable-go + RPC server
|
|
236
227
|
(`frontend/anycable_probe.mjs`, `anycable_concurrent.mjs`): liveness, the
|
|
237
|
-
|
|
228
|
+
yrb-lite client provider, cross-process reads, and concurrent convergence.
|
|
238
229
|
|
|
239
230
|
The wire format is the standard y-protocols binary messages, base64-encoded in
|
|
240
|
-
the ActionCable envelope.
|
|
241
|
-
`{ "update" => ... }
|
|
242
|
-
message per frame, so the off-the-shelf provider works with no custom client
|
|
243
|
-
code:
|
|
231
|
+
the ActionCable envelope. yrb-lite uses one canonical document envelope,
|
|
232
|
+
`{ "update" => ... }`, and sends one message per frame.
|
|
244
233
|
|
|
245
234
|
```js
|
|
246
|
-
import { createConsumer } from "@
|
|
247
|
-
import {
|
|
235
|
+
import { createConsumer } from "@anycable/web"
|
|
236
|
+
import { ActionCableProvider } from "yrb-lite-client"
|
|
248
237
|
|
|
249
|
-
const provider = new
|
|
238
|
+
const provider = new ActionCableProvider(ydoc, createConsumer(), "DocumentChannel", { id: docId })
|
|
239
|
+
provider.connect()
|
|
250
240
|
```
|
|
251
241
|
|
|
252
242
|
[`examples/actioncable-demo`](examples/actioncable-demo) is a full Rails + Tiptap
|
|
253
|
-
app using
|
|
243
|
+
app using the yrb-lite provider, with end-to-end tests.
|
|
254
244
|
|
|
255
|
-
####
|
|
245
|
+
#### Record Before Distribute
|
|
256
246
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
auditing or to guarantee nothing is distributed until it's stored, register an
|
|
260
|
-
`on_change` recorder:
|
|
247
|
+
Every document change is durably recorded before anyone else sees it. Register
|
|
248
|
+
an `on_change` recorder:
|
|
261
249
|
|
|
262
250
|
```ruby
|
|
263
251
|
class DocumentChannel < ApplicationCable::Channel
|
|
264
|
-
include YrbLite::Sync
|
|
252
|
+
include YrbLite::ActionCable::Sync
|
|
265
253
|
|
|
266
254
|
on_change do |key, update|
|
|
267
255
|
# Synchronous, durable write. `update` is the exact CRDT delta.
|
|
@@ -269,78 +257,43 @@ class DocumentChannel < ApplicationCable::Channel
|
|
|
269
257
|
end
|
|
270
258
|
|
|
271
259
|
def subscribed = sync_for(params[:id])
|
|
272
|
-
def receive(data) = sync_receive(data)
|
|
273
|
-
def unsubscribed =
|
|
260
|
+
def receive(data) = sync_receive(data, params[:id])
|
|
261
|
+
def unsubscribed = sync_unsubscribed(params[:id])
|
|
274
262
|
end
|
|
275
263
|
```
|
|
276
264
|
|
|
277
265
|
With `on_change` registered, a change is recorded before it goes anywhere. The
|
|
278
266
|
recorder writes the raw CRDT delta synchronously; only then is the change
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
it's applied. That's what makes the log authoritative. Replay the deltas onto a
|
|
282
|
-
fresh `Y.Doc` and you get the document back exactly.
|
|
267
|
+
broadcast. Replay the deltas onto a fresh `Y.Doc` and you get the document back
|
|
268
|
+
exactly.
|
|
283
269
|
|
|
284
270
|
If the recorder raises (say the store is down), the change is rejected: not
|
|
285
271
|
applied, not sent to anyone. The cost is a synchronous durable write per change,
|
|
286
272
|
which serializes that document's writes. Other documents use other locks and run
|
|
287
273
|
in parallel.
|
|
288
274
|
|
|
289
|
-
`on_change`
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`on_change` to an fsync'd append-only log and checks, end to end, that the log
|
|
293
|
-
alone rebuilds the document.
|
|
275
|
+
The demo wires `on_change` to a durable Postgres-backed log by default, with an
|
|
276
|
+
fsync'd file log available via `STORE_KIND=file`, and checks end to end that the
|
|
277
|
+
log alone rebuilds the document.
|
|
294
278
|
|
|
295
279
|
#### Reliable delivery (acks)
|
|
296
280
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
state everyone *has*; they don't recover an update that never arrived.
|
|
302
|
-
|
|
303
|
-
yrb-lite closes that gap with an opt-in, client-driven acknowledgement. If an
|
|
304
|
-
incoming frame carries an `"id"`, the server replies `{ "ack": <id> }` once the
|
|
305
|
-
update has been **accepted** -- recorded in audit mode, applied in fast mode. A
|
|
306
|
-
causally-gapped update is not acked (it gets a resync instead), so the client
|
|
307
|
-
knows it hasn't landed yet.
|
|
281
|
+
yrb-lite document delivery is ack-tracked. Browser document updates carry an
|
|
282
|
+
`"id"`, and the server replies `{ "ack": <id> }` once the update has been
|
|
283
|
+
**durably recorded**. A causally-gapped update is not acked; the server sends a
|
|
284
|
+
resync request, and the client keeps the update queued until it lands.
|
|
308
285
|
|
|
309
286
|
```
|
|
310
|
-
client -> server { "
|
|
287
|
+
client -> server { "update": "<base64 update>", "id": 42 }
|
|
311
288
|
server -> client { "ack": 42 } # update accepted; safe to forget
|
|
312
289
|
```
|
|
313
290
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
This is entirely **self-gating**: stock clients send no `"id"`, so they never get
|
|
322
|
-
acks and behave exactly as before. Only a client that opts in by tagging its
|
|
323
|
-
frames participates.
|
|
324
|
-
|
|
325
|
-
Two client examples ship in the demo:
|
|
326
|
-
|
|
327
|
-
- [`frontend/reliable.mjs`](examples/actioncable-demo/frontend/reliable.mjs) — a
|
|
328
|
-
minimal reference client showing the raw mechanism (tag with an id, retain,
|
|
329
|
-
retransmit on a timer, drain on ack), with an end-to-end test that loses an
|
|
330
|
-
update mid-flight and recovers it purely by retransmit.
|
|
331
|
-
- [`frontend/provider/reliable_actioncable_provider.mjs`](examples/actioncable-demo/frontend/provider/reliable_actioncable_provider.mjs)
|
|
332
|
-
— the standard `@y-rb/actioncable` `WebsocketProvider`, vendored and augmented
|
|
333
|
-
for production use. It's a drop-in replacement that speaks the same protocol
|
|
334
|
-
and envelope, and adds reliability with **sync-since-last-ack** framing: rather
|
|
335
|
-
than retransmitting updates one by one, it keeps the unacknowledged local
|
|
336
|
-
updates in a queue and sends their *merge* as a single causally-complete delta,
|
|
337
|
-
with the id being the highest sequence in the batch (so one `{ ack: id }`
|
|
338
|
-
cumulatively confirms everything up to it). Because the whole unacked tail goes
|
|
339
|
-
as one self-contained delta, the server never sees an internal gap and never
|
|
340
|
-
has to round-trip a resync for a lost middle update — the next edit, or the
|
|
341
|
-
next timer tick, carries it. Awareness stays fire-and-forget; against a server
|
|
342
|
-
that doesn't implement acks it warns once and falls back to plain delivery; and
|
|
343
|
-
`reliable: false` opts out entirely. The demo's editor uses this provider.
|
|
291
|
+
`yrb-lite-client`'s `ActionCableProvider` handles this automatically. It keeps
|
|
292
|
+
the unacknowledged local document tail in a queue and sends the merged tail as a
|
|
293
|
+
single causally-complete delta. The id is the highest sequence in the batch, so
|
|
294
|
+
one `{ ack: id }` cumulatively confirms everything up to it. Because CRDT apply
|
|
295
|
+
is idempotent, a resend that already landed is a harmless no-op that just
|
|
296
|
+
re-acks. Awareness stays ephemeral and is not acked.
|
|
344
297
|
|
|
345
298
|
### User Awareness/Presence
|
|
346
299
|
|
|
@@ -376,24 +329,24 @@ message = awareness.encode_update(update_bytes)
|
|
|
376
329
|
|
|
377
330
|
## Thread Safety
|
|
378
331
|
|
|
379
|
-
|
|
380
|
-
`
|
|
381
|
-
|
|
332
|
+
`Doc` and `Awareness` are safe to share across Ruby threads. A `Doc` or
|
|
333
|
+
`Awareness` can be used concurrently from Puma workers, ActionCable connection
|
|
334
|
+
threads, or background jobs without external locking.
|
|
382
335
|
|
|
383
336
|
That comes from how the underlying types work, not from locking on top:
|
|
384
337
|
|
|
385
338
|
- `yrs::Doc` is `Send + Sync`. Every operation takes the document's internal
|
|
386
339
|
RwLock with blocking semantics (`read_blocking`/`write_blocking`), so
|
|
387
340
|
concurrent access serializes instead of erroring or corrupting state.
|
|
388
|
-
- `yrs::sync::Awareness` is
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Each native method opens and closes its transaction
|
|
393
|
-
|
|
394
|
-
-
|
|
395
|
-
a yrs upgrade regressed
|
|
396
|
-
turning thread-unsafe.
|
|
341
|
+
- `yrs::sync::Awareness` is `Send` but not `Sync` in the current yrs version,
|
|
342
|
+
so the Ruby wrapper stores it in a `Mutex`. The mutex is always acquired
|
|
343
|
+
inside the no-GVL native section and released before Ruby runs again.
|
|
344
|
+
- The extension uses no `RefCell`-style runtime borrows that could panic under
|
|
345
|
+
re-entrancy. Each native method opens and closes its transaction or mutex
|
|
346
|
+
guard inside one call.
|
|
347
|
+
- Static assertions in `lib.rs` prove `Doc` and `Mutex<Awareness>` are
|
|
348
|
+
`Send + Sync`. If a yrs upgrade regressed either wrapper's thread-safety, the
|
|
349
|
+
gem would fail to compile instead of quietly turning thread-unsafe.
|
|
397
350
|
|
|
398
351
|
`test/thread_safety_test.rb` runs shared docs, the full sync handshake, fan-in
|
|
399
352
|
sync, and awareness state across 8 threads at once, and checks the interleaving
|
|
@@ -414,11 +367,12 @@ A slow operation also can't stall the VM. A thread applying a large update holds
|
|
|
414
367
|
the doc's write lock without holding the GVL, so other Ruby threads keep running
|
|
415
368
|
instead of queuing behind it.
|
|
416
369
|
|
|
417
|
-
Each method has the same shape: copy
|
|
418
|
-
yrs work
|
|
419
|
-
the GVL back, then build Ruby objects. No Ruby API is touched
|
|
420
|
-
|
|
421
|
-
deadlock. Panics in native code are caught and re-raised as Ruby
|
|
370
|
+
Each method has the same shape: copy Ruby byte strings first, drop the GVL, do
|
|
371
|
+
the yrs work while taking and releasing native locks entirely inside the
|
|
372
|
+
closure, take the GVL back, then build Ruby objects. No Ruby API is touched
|
|
373
|
+
without the GVL, and no native lock is held while reacquiring it, so the lock
|
|
374
|
+
order can't deadlock. Panics in native code are caught and re-raised as Ruby
|
|
375
|
+
exceptions.
|
|
422
376
|
|
|
423
377
|
## Message Type Constants
|
|
424
378
|
|
|
Binary file
|
|
Binary file
|
data/lib/yrb_lite/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yrb-lite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.beta6
|
|
5
5
|
platform: arm64-darwin
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|