yrb-lite 0.1.0.beta5-aarch64-linux → 0.1.0.beta6-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: 5fce4e6337e2a2df0f2f44db2b5df7447a1b05b2cddd5976d99975c6a1fa62a1
4
- data.tar.gz: 55c409d86e2f27b69eac45e2f909b8a87d3ff4ef5e8cb7b7e68db3c94b1174fe
3
+ metadata.gz: 2730b0d6a2bc31a36de1f842d7345659788d4c45968a895dfbbf7855ee6be21d
4
+ data.tar.gz: 9731717390fc3e45c983f20dd51429c214a6aaca85cb75b0e375a34390a785e0
5
5
  SHA512:
6
- metadata.gz: ad0f9a7c036b495e331ea12c0929fc9d7a0dbbebdf5e65e5477366ee60f744b37b25c3fd590a33a4b7434091c30d3ef2865b8d7ef5a3fdb426fe11b3d0e265b9
7
- data.tar.gz: 5fe28a7bc60182a5e268baf5543ea10dc57b6950951f4c18db606db60422759cd8f6b4be5caf04bbfba56938aad283a8cd41e96ca5e55ddd63d0a94354c3cba3
6
+ metadata.gz: '0913343e81dd6564a2ce23f058ec63cadb23917a105a86e430d859df77856054a8c0e4c02051a28a0dc8903c916f8c7ea60dd051ae8cbc022064d28539610680'
7
+ data.tar.gz: fb98a8a3e81ec173f812618dce6075a84f6516e4c7991c650c35835958465312b5b0380176a1e5628fdf302de8faa0ec05d3df64b1f78533c96820825ec4d2bc
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. `on_load`/`on_save` are unchanged: they can run in the shared
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
- accepted (recorded in audit mode, applied in fast mode). This lets an
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. Stock clients send no
73
- `"id"`, never get acks, and behave exactly as before.
74
- - A vendored, ack-aware `@y-rb/actioncable` provider in the demo
75
- (`reliable_actioncable_provider.mjs`) that adds reliable delivery with
76
- "sync-since-last-ack" framing (the unacknowledged tail is sent as one merged,
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). It's wire-compatible with
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
- - Two backends: `sync_backend :memory` (default, classic ActionCable) and
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 = sync_clear_presence
16
+ def unsubscribed = sync_unsubscribed(params[:id])
17
17
  end
18
18
  ```
19
19
 
20
- On the browser, use the [`@y-rb/actioncable`](https://www.npmjs.com/package/@y-rb/actioncable)
21
- provider as-is. Tiptap, ProseMirror, and BlockNote all sync through it.
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 across Puma threads,
26
- and the GVL is released while yrs does the actual work.
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
- - A store-backed mode for AnyCable and multi-process deployments.
30
- - An optional authoritative mode that records each change durably before it
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, and an end-to-end suite runs the real
40
- stack: it fuzzes the protocol, throws garbage and chaos at the server, kills the
41
- server mid-write to check crash recovery, and drives real browsers under load.
42
- The benchmark numbers below are from a single laptop. Issues and PRs are
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. Precompiled gems ship for Linux and macOS on Ruby
52
- 3.4 and 4.0, so installing there needs no Rust. Other platforms (and any other
53
- Ruby version) build from source, which needs [Rust](https://rustup.rs).
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` is a channel concern that implements the full y-websocket
132
- protocol (document sync + awareness/presence) over ActionCable:
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
- # Optional persistence:
140
- # on_load { |key| Document.find_by(key: key)&.content }
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
- sync_clear_presence
158
+ sync_unsubscribed(params[:id])
153
159
  end
154
160
  end
155
161
  ```
156
162
 
157
- One `YrbLite::Awareness` is shared per document key. Creating it is
158
- mutex-serialized; after that everything runs lock-free on the thread-safe
159
- native types. The concern answers SyncStep1 directly, relays document and
160
- awareness changes to the other subscribers (not back to the sender), and calls
161
- `on_save` after any message that changed the document.
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
- `sync_unsubscribed` clears the connection's presence, so a closed tab doesn't
164
- leave a stale cursor hanging until the client-side timeout. It also unloads the
165
- document from memory once the last subscriber disconnects, which keeps the
166
- process from holding onto every document it ever served. That unload only
167
- happens when `on_load` is set and the document can be reloaded later; without
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
- Each process also keeps its own copy of the document and applies broadcasts from
186
- the others. The merge is an ordinary CRDT apply, idempotent and
187
- order-independent, which keeps server reads and new-client handshakes current on
188
- every process. Each broadcast carries a per-process id (`Sync.process_id`) that
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
- the lot: convergence, fresh copies on both, presence across processes, and one
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
- The default backend keeps that warm in-memory copy and relies on a `stream_from`
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
- `@y-rb/actioncable` provider, cross-process reads, and concurrent convergence.
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. The server accepts the `@y-rb/actioncable` provider's
241
- `{ "update" => ... }` envelope (and its own `{ "m" => ... }`) and sends one
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 "@rails/actioncable"
247
- import { WebsocketProvider } from "@y-rb/actioncable"
235
+ import { createConsumer } from "@anycable/web"
236
+ import { ActionCableProvider } from "yrb-lite-client"
248
237
 
249
- const provider = new WebsocketProvider(ydoc, createConsumer(), "DocumentChannel", { id: docId })
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 that provider, with end-to-end tests.
243
+ app using the yrb-lite provider, with end-to-end tests.
254
244
 
255
- #### Authoritative audit mode (record before distribute)
245
+ #### Record Before Distribute
256
246
 
257
- By default a change is applied and broadcast immediately (the fast path). If you
258
- need to durably record every change before anyone else sees it, whether for
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 = sync_clear_presence
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
- applied to the shared document and broadcast. The whole sequence runs under a
280
- per-document lock, so every change to a document is recorded in the same order
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` and `on_save` are separate. `on_save` snapshots the whole document
290
- when it gets a chance; `on_change` is the per-change log. The demo's `AUDIT=1`
291
- mode (in [`examples/actioncable-demo`](examples/actioncable-demo)) wires
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
- The y-websocket protocol is fire-and-forget. If a client's update is lost in
298
- transit (a flaky socket, a send that never lands) and the client makes no
299
- further edits, the server stays idle and never asks anyone to resync, so that
300
- edit is gone -- even though the client believes it was saved. CRDTs converge the
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 { "m": "<base64 update>", "id": 42 }
287
+ client -> server { "update": "<base64 update>", "id": 42 }
311
288
  server -> client { "ack": 42 } # update accepted; safe to forget
312
289
  ```
313
290
 
314
- That's the whole server side. A reliable client tags each outgoing update with
315
- an incrementing id, keeps it in a pending buffer, and retransmits on a timer (and
316
- on reconnect) until the matching ack returns. Because CRDT apply is idempotent, a
317
- resend that already landed is a harmless no-op that just re-acks. An update lost
318
- in transit is recovered by the client's own retransmit -- no reconnect required,
319
- and no dependence on a later edit happening to trigger a resync.
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
- Unlike the official `y-rb` gem, yrb-lite is safe to share across Ruby threads. A
380
- `Doc` or `Awareness` can be used concurrently from Puma workers, ActionCable
381
- connection threads, or background jobs without external locking.
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 built for multi-threaded servers: client states
389
- live in a `DashMap` and the whole API is `&self`.
390
- - The extension adds no interior-mutability tricks. There's no `RefCell`, where
391
- a re-entrant borrow would panic and take the Ruby process down with it.
392
- Each native method opens and closes its transaction in one call, so no lock
393
- or borrow outlives a call and there's nothing to deadlock on.
394
- - A `Send + Sync` static assertion for both wrapped types lives in `lib.rs`. If
395
- a yrs upgrade regressed this, the gem would fail to compile instead of quietly
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 the Ruby byte string, drop the GVL, do the
418
- yrs work (taking and releasing the doc lock entirely inside the closure), take
419
- the GVL back, then build Ruby objects. No Ruby API is touched without the GVL,
420
- and the doc lock is never held across a GVL boundary, so the lock order can't
421
- deadlock. Panics in native code are caught and re-raised as Ruby exceptions.
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
@@ -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.beta6"
5
5
  end
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.beta5
4
+ version: 0.1.0.beta6
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-06-19 00:00:00.000000000 Z
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest