yrb-lite 0.1.0.beta6 → 0.1.0.beta9
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 +0 -132
- data/README.md +50 -67
- data/ext/yrb_lite/src/lib.rs +53 -443
- data/ext/yrb_lite/src/protocol.rs +15 -326
- data/lib/yrb_lite/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d834b5052572c1d84374702a2c45963522725abf47eaaefd62acc934b75a4fe
|
|
4
|
+
data.tar.gz: 025f4e5ae96c0fd3c9af6e43e2ddd16c4bc22cb34b95b17ad70f8ff74283557b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73e9cd5d0bf9ffbf3c729f54390731376dd443d1f59fa597917c926f3c6326089903ab7b9c67a2dc037de343368d6f1f045f937fae99a2c01d430500fd0f6fe5
|
|
7
|
+
data.tar.gz: 67ad34f34202019617905d035e6cfb91f3693534eebed93d3fbb612b1e410bc75803f4bc2c311bedff076bed15c3718a4100153e3b42ae1de74e9cae0d7f73a5
|
data/CHANGELOG.md
CHANGED
|
@@ -5,135 +5,3 @@ All notable changes to this project are documented here. The format is based on
|
|
|
5
5
|
to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
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
|
-
|
|
38
|
-
## [0.1.0.beta5] - 2026-06-18
|
|
39
|
-
|
|
40
|
-
### Changed
|
|
41
|
-
|
|
42
|
-
- **Breaking:** the ActionCable integration has been extracted into a separate
|
|
43
|
-
gem, [`yrb-lite-actioncable`](https://rubygems.org/gems/yrb-lite-actioncable).
|
|
44
|
-
`yrb-lite` is now a standalone y-crdt wrapper: CRDT documents, awareness, and
|
|
45
|
-
the y-websocket sync protocol primitives, with no Rails/ActionCable coupling
|
|
46
|
-
(mirrors the `y-rb` / `yrb-actioncable` split). The `base64` runtime
|
|
47
|
-
dependency moved with it.
|
|
48
|
-
|
|
49
|
-
### Migration
|
|
50
|
-
|
|
51
|
-
- Using `YrbLite::Sync`? Add `gem "yrb-lite-actioncable"` and change
|
|
52
|
-
`include YrbLite::Sync` to `include YrbLite::ActionCable::Sync`. The concern's
|
|
53
|
-
API is otherwise unchanged. If you only use `YrbLite::Doc`/`YrbLite::Awareness`,
|
|
54
|
-
nothing changes.
|
|
55
|
-
|
|
56
|
-
## [0.1.0.beta4] - 2026-06-18
|
|
57
|
-
|
|
58
|
-
### Changed
|
|
59
|
-
|
|
60
|
-
- `on_change` block recorders now run in the **channel instance's context**
|
|
61
|
-
(via `instance_exec`), so a recorder can call the channel's own methods --
|
|
62
|
-
`current_user`, `params`, request/connection-scoped accessors -- directly,
|
|
63
|
-
instead of plumbing them in through a thread-local. A non-Proc callable (an
|
|
64
|
-
object responding to `#call`) is still invoked with `#call` and its own
|
|
65
|
-
context. Existing block recorders that use
|
|
66
|
-
only the `(key, update)` arguments and lexically-scoped constants are
|
|
67
|
-
unaffected; the only behavioral change is `self` inside the block.
|
|
68
|
-
|
|
69
|
-
## [0.1.0.beta3] - 2026-06-18
|
|
70
|
-
|
|
71
|
-
### Changed
|
|
72
|
-
|
|
73
|
-
- Upgraded the bundled `yrs` (y-crdt) from 0.21 to 0.27.2. No change to the
|
|
74
|
-
`YrbLite::Doc`, `YrbLite::Awareness`, or `YrbLite::Sync` public API; existing
|
|
75
|
-
code and the wire protocol are unaffected.
|
|
76
|
-
- Thread-safety is preserved across the upgrade. yrs 0.27 dropped `Awareness`'s
|
|
77
|
-
internal locking (its mutating methods now take `&mut self`, and `Awareness`
|
|
78
|
-
is no longer `Sync`), so `YrbLite::Awareness` now serializes access through an
|
|
79
|
-
internal `Mutex`. The lock is taken only while the GVL is released and is
|
|
80
|
-
never held across the GVL boundary, so concurrent access from multiple Ruby
|
|
81
|
-
threads stays safe and deadlock-free, and document reads still run in parallel
|
|
82
|
-
(they operate on a cheaply-cloned, `Arc`-backed `Doc` handle, not under the
|
|
83
|
-
presence lock).
|
|
84
|
-
|
|
85
|
-
### Build
|
|
86
|
-
|
|
87
|
-
- Building the gem from source now requires **Rust 1.94 or newer** (yrs 0.27.2
|
|
88
|
-
uses `let`-chains). The precompiled platform gems are unaffected -- they need
|
|
89
|
-
no Rust toolchain to install.
|
|
90
|
-
|
|
91
|
-
## [0.1.0.beta2] - 2026-06-16
|
|
92
|
-
|
|
93
|
-
### Added
|
|
94
|
-
|
|
95
|
-
- Reliable delivery (opt-in, client-driven). A client may tag a document update
|
|
96
|
-
with an `"id"`; the server replies `{ "ack": <id> }` once the update has been
|
|
97
|
-
durably recorded. This lets an
|
|
98
|
-
ack-aware client retain and retransmit an update until delivery is confirmed,
|
|
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.
|
|
104
|
-
|
|
105
|
-
### Fixed
|
|
106
|
-
|
|
107
|
-
- Causal-gap protection. The authoritative, fast, and store paths now reject a
|
|
108
|
-
document update that isn't causally ready -- one whose dependencies are
|
|
109
|
-
missing because an earlier update was lost in transit or its durable record
|
|
110
|
-
failed -- and ask the client to resync, instead of recording or relaying an
|
|
111
|
-
un-integrable update that would leave the log permanently pending. Adds native
|
|
112
|
-
`Doc#update_ready?`/`#pending?` (cheap, read-only checks) used to gate the
|
|
113
|
-
record-before-distribute path.
|
|
114
|
-
|
|
115
|
-
## [0.1.0.beta1]
|
|
116
|
-
|
|
117
|
-
### Added
|
|
118
|
-
|
|
119
|
-
- Thread-safe `YrbLite::Doc` and `YrbLite::Awareness` over `yrs` (magnus/rb-sys
|
|
120
|
-
native extension). The GVL is released during CRDT work so docs can run in
|
|
121
|
-
parallel on MRI.
|
|
122
|
-
- `YrbLite::Sync` ActionCable channel concern implementing the y-websocket
|
|
123
|
-
protocol (document sync plus awareness/presence).
|
|
124
|
-
- A "record-before-distribute" mode via an `on_change` hook, so every change is
|
|
125
|
-
recorded durably before it's applied or relayed.
|
|
126
|
-
- Presence cleanup on disconnect, and idle-document eviction.
|
|
127
|
-
- Store-backed ActionCable delivery for AnyCable and multi-process use.
|
|
128
|
-
- Hardening against bad input: malformed or multi-message frames are dropped
|
|
129
|
-
before processing or relay, and native panics are contained at the FFI
|
|
130
|
-
boundary.
|
|
131
|
-
- Precompiled native gems for common platforms (no Rust toolchain needed to
|
|
132
|
-
install) via the cross-gem workflow.
|
|
133
|
-
|
|
134
|
-
[Unreleased]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta5...main
|
|
135
|
-
[0.1.0.beta5]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta4...v0.1.0.beta5
|
|
136
|
-
[0.1.0.beta4]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta3...v0.1.0.beta4
|
|
137
|
-
[0.1.0.beta3]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta2...v0.1.0.beta3
|
|
138
|
-
[0.1.0.beta2]: https://github.com/jpcamara/yrb-lite/compare/v0.1.0.beta1...v0.1.0.beta2
|
|
139
|
-
[0.1.0.beta1]: https://github.com/jpcamara/yrb-lite/releases/tag/v0.1.0.beta1
|
data/README.md
CHANGED
|
@@ -19,12 +19,12 @@ end
|
|
|
19
19
|
|
|
20
20
|
On the browser, use the `yrb-lite-client` `ActionCableProvider`. Tiptap,
|
|
21
21
|
ProseMirror, and BlockNote all sync through the `Y.Doc` you pass in and the
|
|
22
|
-
provider's Awareness instance
|
|
22
|
+
provider's Awareness instance.
|
|
23
23
|
|
|
24
24
|
## What you get
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
|
|
26
|
+
- A thread-safe Ruby `Doc` you can share across Puma threads; native CRDT work
|
|
27
|
+
runs with the GVL released.
|
|
28
28
|
- The y-websocket protocol (document sync plus awareness/presence) as a
|
|
29
29
|
one-include ActionCable concern.
|
|
30
30
|
- Store-backed ActionCable/AnyCable delivery for multi-process deployments.
|
|
@@ -86,11 +86,7 @@ require "yrb_lite"
|
|
|
86
86
|
|
|
87
87
|
# Create docs
|
|
88
88
|
doc = YrbLite::Doc.new # random client ID
|
|
89
|
-
doc = YrbLite::Doc.new(12345) # specific client ID
|
|
90
|
-
|
|
91
|
-
# Get document info
|
|
92
|
-
doc.client_id # => unique client identifier
|
|
93
|
-
doc.guid # => document GUID
|
|
89
|
+
doc = YrbLite::Doc.new(12345) # specific client ID (used for CRDT identity)
|
|
94
90
|
|
|
95
91
|
# Encoding
|
|
96
92
|
doc.encode_state_vector # => current state vector
|
|
@@ -100,36 +96,23 @@ doc.encode_state_as_update(sv) # => update diff against state vector
|
|
|
100
96
|
# Applying updates
|
|
101
97
|
doc.apply_update(update_bytes) # apply raw V1 update
|
|
102
98
|
|
|
103
|
-
# Sync protocol
|
|
104
|
-
doc.sync_step1 # => SyncStep1 message (
|
|
105
|
-
doc.
|
|
106
|
-
|
|
107
|
-
doc.encode_update_message(update) # => wrap update as sync Update message
|
|
99
|
+
# Sync protocol
|
|
100
|
+
doc.sync_step1 # => SyncStep1 message (this doc's state vector)
|
|
101
|
+
doc.handle_sync_message(data) # => [msg_type, sync_type, response]; answers a
|
|
102
|
+
# peer's SyncStep1 with a SyncStep2
|
|
108
103
|
```
|
|
109
104
|
|
|
110
|
-
###
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
# Create awareness instances (each contains a Doc)
|
|
114
|
-
awareness = YrbLite::Awareness.new # random client ID
|
|
115
|
-
awareness = YrbLite::Awareness.new(12345) # specific client ID
|
|
116
|
-
|
|
117
|
-
# Get document info
|
|
118
|
-
awareness.client_id # => unique client identifier
|
|
119
|
-
awareness.guid # => document GUID
|
|
120
|
-
```
|
|
105
|
+
### Protocol codec (module functions)
|
|
121
106
|
|
|
122
|
-
|
|
107
|
+
Classifying and unwrapping wire frames is stateless, so it's exposed as
|
|
108
|
+
`YrbLite` module functions rather than a class. The server never holds presence
|
|
109
|
+
or document state to route a frame — presence lives in the browser clients, and
|
|
110
|
+
the server only relays awareness frames opaquely.
|
|
123
111
|
|
|
124
112
|
```ruby
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
# When receiving messages from peer
|
|
130
|
-
response = awareness.handle(incoming_data)
|
|
131
|
-
# Send response back to peer if not empty
|
|
132
|
-
send_to_peer(response) unless response.empty?
|
|
113
|
+
YrbLite.message_kind(frame) # => 0 drop / 1 step1 / 2 update / 3 awareness / 4 query
|
|
114
|
+
YrbLite.update_from_message(frame) # => the document delta carried by a frame, or nil
|
|
115
|
+
YrbLite.wrap_update(update_bytes) # => wrap a raw doc update as a sync Update frame
|
|
133
116
|
```
|
|
134
117
|
|
|
135
118
|
### ActionCable Integration
|
|
@@ -178,6 +161,34 @@ oversized, or unknown frames are dropped. A bad frame can't crash the process: a
|
|
|
178
161
|
Rust panic is caught at the FFI boundary and re-raised as a Ruby exception. And
|
|
179
162
|
no single client can relay garbage that breaks the others in a room.
|
|
180
163
|
|
|
164
|
+
#### Delivery guarantees
|
|
165
|
+
|
|
166
|
+
The contract is the same at every scale — one process, or hundreds across many
|
|
167
|
+
servers:
|
|
168
|
+
|
|
169
|
+
- **The document always converges.** CRDT updates are commutative and
|
|
170
|
+
idempotent, so out-of-order, duplicate, or concurrent delivery all converge to
|
|
171
|
+
the same correct document. This needs no coordination and holds everywhere.
|
|
172
|
+
- **The durable log never goes gappy.** An update is recorded only once its
|
|
173
|
+
causal dependencies are already in the store (checked against `on_load`); a
|
|
174
|
+
causally-incomplete update triggers a resync instead, so the log always
|
|
175
|
+
rebuilds cleanly.
|
|
176
|
+
- **`on_change` is at-least-once, and the durable guarantee is that replaying the
|
|
177
|
+
log reconstructs the document.** Every change is recorded before it's acked or
|
|
178
|
+
broadcast (record-before-distribute). Entry count is not 1:1 with edits: a
|
|
179
|
+
best-effort check skips most lost-ack retries but isn't cross-process exact (a
|
|
180
|
+
retry on another process can record the same update twice), and a resync can
|
|
181
|
+
coalesce a client's un-acked tail into a single record. So **make `on_change`
|
|
182
|
+
idempotent** if duplicate side effects would matter (a webhook, a counter) — a
|
|
183
|
+
raw append-only delta log is naturally fine, since it replays to the same
|
|
184
|
+
document either way.
|
|
185
|
+
|
|
186
|
+
There is deliberately no in-gem cross-process lock. One that only spanned a
|
|
187
|
+
single process would give exactly-once at small scale and silently degrade as
|
|
188
|
+
you scale out, so the guarantee is uniform instead. If you need exactly-once
|
|
189
|
+
*side effects*, enforce it in your store (a unique key on the update) or with
|
|
190
|
+
your own distributed lock — the gem stays storage-agnostic and assumes neither.
|
|
191
|
+
|
|
181
192
|
#### Multi-process deployments
|
|
182
193
|
|
|
183
194
|
Most Rails apps run several processes (Puma workers, multiple dynos), and any of
|
|
@@ -295,43 +306,15 @@ one `{ ack: id }` cumulatively confirms everything up to it. Because CRDT apply
|
|
|
295
306
|
is idempotent, a resend that already landed is a harmless no-op that just
|
|
296
307
|
re-acks. Awareness stays ephemeral and is not acked.
|
|
297
308
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
# Set local user state (cursor position, name, etc.)
|
|
302
|
-
awareness.set_local_state('{"user": {"name": "Alice", "color": "#ff0000"}}')
|
|
303
|
-
|
|
304
|
-
# Get local state
|
|
305
|
-
awareness.local_state # => '{"user": {"name": "Alice", "color": "#ff0000"}}'
|
|
306
|
-
|
|
307
|
-
# Clear local state (e.g., when disconnecting)
|
|
308
|
-
awareness.clear_local_state
|
|
309
|
-
|
|
310
|
-
# Encode awareness update for broadcasting
|
|
311
|
-
update = awareness.encode_awareness_update
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Low-Level Access
|
|
315
|
-
|
|
316
|
-
```ruby
|
|
317
|
-
# Get state vector for manual sync
|
|
318
|
-
sv = awareness.encode_state_vector
|
|
319
|
-
|
|
320
|
-
# Get update diffed against a state vector
|
|
321
|
-
update = awareness.encode_state_as_update(remote_state_vector)
|
|
322
|
-
|
|
323
|
-
# Apply raw update to the document
|
|
324
|
-
awareness.apply_update(update_bytes)
|
|
325
|
-
|
|
326
|
-
# Wrap raw update data in a sync message
|
|
327
|
-
message = awareness.encode_update(update_bytes)
|
|
328
|
-
```
|
|
309
|
+
Presence (cursors, selections) is owned by the browser clients — the server
|
|
310
|
+
never sets or holds presence state, it only relays awareness frames opaquely.
|
|
311
|
+
See `yrb-lite-client` for the client-side awareness API.
|
|
329
312
|
|
|
330
313
|
## Thread Safety
|
|
331
314
|
|
|
332
|
-
`Doc`
|
|
333
|
-
|
|
334
|
-
|
|
315
|
+
A `Doc` is safe to share across Ruby threads — used concurrently from Puma
|
|
316
|
+
workers, ActionCable connection threads, or background jobs without external
|
|
317
|
+
locking.
|
|
335
318
|
|
|
336
319
|
That comes from how the underlying types work, not from locking on top:
|
|
337
320
|
|
data/ext/yrb_lite/src/lib.rs
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
use magnus::{
|
|
2
2
|
function, method, prelude::*, Error, ExceptionClass, RString, Ruby, TryConvert, Value,
|
|
3
3
|
};
|
|
4
|
-
use
|
|
5
|
-
use yrs::sync::{Awareness, DefaultProtocol, Message, Protocol, SyncMessage};
|
|
4
|
+
use yrs::sync::{Message, SyncMessage};
|
|
6
5
|
use yrs::updates::decoder::Decode;
|
|
7
|
-
use yrs::updates::encoder::
|
|
8
|
-
use yrs::{
|
|
6
|
+
use yrs::updates::encoder::Encode;
|
|
7
|
+
use yrs::{Doc, ReadTxn, Transact};
|
|
9
8
|
|
|
10
9
|
mod protocol;
|
|
11
|
-
use protocol::{
|
|
12
|
-
awareness_client_ids_in, classify_message, doc_has_pending, merged_doc_update,
|
|
13
|
-
update_advances_doc, update_is_ready, validate_frame_client_ids,
|
|
14
|
-
validate_state_vector_client_ids, validate_update_client_ids,
|
|
15
|
-
};
|
|
10
|
+
use protocol::{classify_message, merged_doc_update, update_advances_doc, update_is_ready};
|
|
16
11
|
|
|
17
12
|
/// Wrapper around yrs Doc.
|
|
18
13
|
///
|
|
@@ -24,37 +19,13 @@ use protocol::{
|
|
|
24
19
|
#[magnus::wrap(class = "YrbLite::Doc", free_immediately, size)]
|
|
25
20
|
struct RbDoc(Doc);
|
|
26
21
|
|
|
27
|
-
///
|
|
28
|
-
///
|
|
29
|
-
///
|
|
30
|
-
/// its mutating methods (`handle`, `set_local_state`, `clean_local_state`,
|
|
31
|
-
/// `remove_state`, `update_with_clients`) take `&mut self`. It is `Send` but no
|
|
32
|
-
/// longer `Sync`, so we serialize all access through a `Mutex`.
|
|
33
|
-
///
|
|
34
|
-
/// CRITICAL: the `Mutex` is ALWAYS locked inside the `nogvl` closure (never with
|
|
35
|
-
/// the GVL held) and the guard is dropped before the closure returns. This obeys
|
|
36
|
-
/// the same rule as the doc's RwLock (see `nogvl`): a thread never waits on this
|
|
37
|
-
/// lock while holding the GVL, and never reacquires the GVL while holding this
|
|
38
|
-
/// lock, so the GVL and this `Mutex` can't deadlock on lock order. Locking with
|
|
39
|
-
/// the GVL held (outside `nogvl`) reintroduces that deadlock -- don't.
|
|
40
|
-
///
|
|
41
|
-
/// For doc-only reads we clone the (Arc-backed) `Doc` out under the brief lock
|
|
42
|
-
/// and operate on the owned clone, so a long encode holds only the doc's own
|
|
43
|
-
/// RwLock, not this `Mutex`, and never blocks presence updates on another
|
|
44
|
-
/// thread. Lock order is always Mutex-then-doc-RwLock (or doc-RwLock alone),
|
|
45
|
-
/// never the reverse.
|
|
46
|
-
#[magnus::wrap(class = "YrbLite::Awareness", free_immediately, size)]
|
|
47
|
-
struct RbAwareness(Mutex<Awareness>);
|
|
48
|
-
|
|
49
|
-
/// Compile-time proof that the wrapped types are thread-safe. If a future
|
|
50
|
-
/// yrs upgrade makes Doc lose Send/Sync, or Awareness lose Send, this fails the
|
|
51
|
-
/// build instead of silently shipping a thread-unsafe gem. (Awareness is no
|
|
52
|
-
/// longer `Sync` as of yrs 0.27, hence the `Mutex` wrapper, which restores it.)
|
|
22
|
+
/// Compile-time proof that the wrapped Doc is thread-safe. If a future yrs
|
|
23
|
+
/// upgrade makes Doc lose Send/Sync, this fails the build instead of silently
|
|
24
|
+
/// shipping a thread-unsafe gem.
|
|
53
25
|
#[allow(dead_code)]
|
|
54
26
|
fn assert_thread_safe() {
|
|
55
27
|
fn is_send_sync<T: Send + Sync>() {}
|
|
56
28
|
is_send_sync::<Doc>();
|
|
57
|
-
is_send_sync::<Mutex<Awareness>>();
|
|
58
29
|
}
|
|
59
30
|
|
|
60
31
|
/// Run `f` with the GVL (Global VM Lock) released, so other Ruby threads,
|
|
@@ -131,9 +102,9 @@ fn copy_bytes(s: RString) -> Vec<u8> {
|
|
|
131
102
|
}
|
|
132
103
|
|
|
133
104
|
/// Build a `YrbLite::Error` (the gem's own error class, defined in `init`) so
|
|
134
|
-
/// native decode/apply
|
|
135
|
-
///
|
|
136
|
-
///
|
|
105
|
+
/// native decode/apply failures surface as a project-specific error rather than
|
|
106
|
+
/// a generic RuntimeError. Falls back to RuntimeError only if the class somehow
|
|
107
|
+
/// can't be resolved.
|
|
137
108
|
fn yrb_error(msg: String) -> Error {
|
|
138
109
|
let ruby = Ruby::get().unwrap();
|
|
139
110
|
let class = ruby
|
|
@@ -142,26 +113,10 @@ fn yrb_error(msg: String) -> Error {
|
|
|
142
113
|
Error::new(class, msg)
|
|
143
114
|
}
|
|
144
115
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const MAX_SAFE_CLIENT_ID: u64 = (1 << 53) - 1;
|
|
150
|
-
|
|
151
|
-
/// Pure predicate (no Ruby), so the boundary is unit-testable without a VM.
|
|
152
|
-
pub(crate) fn is_safe_client_id(id: u64) -> bool {
|
|
153
|
-
id <= MAX_SAFE_CLIENT_ID
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
fn validate_client_id(id: u64) -> Result<u64, Error> {
|
|
157
|
-
if !is_safe_client_id(id) {
|
|
158
|
-
return Err(yrb_error(format!(
|
|
159
|
-
"client_id {id} exceeds the maximum safe integer ({MAX_SAFE_CLIENT_ID} = 2^53 - 1); \
|
|
160
|
-
Yjs client IDs must be JS-safe integers to avoid collisions"
|
|
161
|
-
)));
|
|
162
|
-
}
|
|
163
|
-
Ok(id)
|
|
164
|
-
}
|
|
116
|
+
// CLIENT IDs ARE NOT VALIDATED -- whoever supplies the id (the app via
|
|
117
|
+
// `Doc.new(id)` / `Awareness.new(id)`, or a remote peer over the wire) is
|
|
118
|
+
// responsible for keeping it JS-safe (<= 2^53 - 1). See the protocol.rs header
|
|
119
|
+
// for why (and `ClientID::try_new`, proposed upstream, for strict rejection).
|
|
165
120
|
|
|
166
121
|
// ============================================================================
|
|
167
122
|
// Doc Implementation
|
|
@@ -173,23 +128,12 @@ impl RbDoc {
|
|
|
173
128
|
let doc = if args.is_empty() {
|
|
174
129
|
Doc::new()
|
|
175
130
|
} else {
|
|
176
|
-
let client_id =
|
|
131
|
+
let client_id: u64 = TryConvert::try_convert(args[0])?;
|
|
177
132
|
Doc::with_client_id(client_id)
|
|
178
133
|
};
|
|
179
134
|
Ok(RbDoc(doc))
|
|
180
135
|
}
|
|
181
136
|
|
|
182
|
-
/// Get the client ID
|
|
183
|
-
fn client_id(&self) -> u64 {
|
|
184
|
-
self.0.client_id().get()
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/// Get the document GUID
|
|
188
|
-
fn guid(&self) -> String {
|
|
189
|
-
self.0.guid().to_string()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/// Get the current state vector encoded as bytes
|
|
193
137
|
fn encode_state_vector(&self) -> RString {
|
|
194
138
|
let doc = &self.0;
|
|
195
139
|
let sv = nogvl(move || {
|
|
@@ -220,12 +164,10 @@ impl RbDoc {
|
|
|
220
164
|
Ok(binary_string(&update))
|
|
221
165
|
}
|
|
222
166
|
|
|
223
|
-
/// Apply a V1 update to the document
|
|
224
167
|
fn apply_update(&self, update: RString) -> Result<(), Error> {
|
|
225
168
|
let update_bytes = copy_bytes(update);
|
|
226
169
|
let doc = &self.0;
|
|
227
170
|
nogvl(move || -> Result<(), String> {
|
|
228
|
-
validate_update_client_ids(&update_bytes)?;
|
|
229
171
|
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
230
172
|
let mut txn = doc.transact_mut();
|
|
231
173
|
txn.apply_update(update).map_err(|e| e.to_string())
|
|
@@ -251,13 +193,6 @@ impl RbDoc {
|
|
|
251
193
|
nogvl(move || update_advances_doc(doc, &update_bytes)).map_err(yrb_error)
|
|
252
194
|
}
|
|
253
195
|
|
|
254
|
-
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
255
|
-
/// missing dependency.
|
|
256
|
-
fn pending(&self) -> bool {
|
|
257
|
-
let doc = &self.0;
|
|
258
|
-
nogvl(move || doc_has_pending(doc))
|
|
259
|
-
}
|
|
260
|
-
|
|
261
196
|
/// Sync step 1: Create a sync message with our state vector
|
|
262
197
|
fn sync_step1(&self) -> RString {
|
|
263
198
|
let doc = &self.0;
|
|
@@ -269,21 +204,6 @@ impl RbDoc {
|
|
|
269
204
|
binary_string(&encoded)
|
|
270
205
|
}
|
|
271
206
|
|
|
272
|
-
/// Sync step 2: Create a sync message with updates for the given state vector
|
|
273
|
-
fn sync_step2(&self, sv_bytes: RString) -> Result<RString, Error> {
|
|
274
|
-
let sv_data = copy_bytes(sv_bytes);
|
|
275
|
-
let doc = &self.0;
|
|
276
|
-
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
277
|
-
validate_state_vector_client_ids(&sv_data)?;
|
|
278
|
-
let sv = yrs::StateVector::decode_v1(&sv_data).map_err(|e| e.to_string())?;
|
|
279
|
-
let txn = doc.transact();
|
|
280
|
-
let update = txn.encode_state_as_update_v1(&sv);
|
|
281
|
-
Ok(Message::Sync(SyncMessage::SyncStep2(update)).encode_v1())
|
|
282
|
-
})
|
|
283
|
-
.map_err(yrb_error)?;
|
|
284
|
-
Ok(binary_string(&encoded))
|
|
285
|
-
}
|
|
286
|
-
|
|
287
207
|
/// Handle a sync message and return response (if any)
|
|
288
208
|
/// Returns [message_type, sync_type, response_bytes] or nil
|
|
289
209
|
fn handle_sync_message(&self, data: RString) -> Result<Option<(u8, u8, RString)>, Error> {
|
|
@@ -292,7 +212,6 @@ impl RbDoc {
|
|
|
292
212
|
|
|
293
213
|
let (msg_type, sync_type, response) =
|
|
294
214
|
nogvl(move || -> Result<(u8, u8, Vec<u8>), String> {
|
|
295
|
-
validate_frame_client_ids(&data_bytes)?;
|
|
296
215
|
let msg = Message::decode_v1(&data_bytes).map_err(|e| e.to_string())?;
|
|
297
216
|
|
|
298
217
|
match msg {
|
|
@@ -331,309 +250,46 @@ impl RbDoc {
|
|
|
331
250
|
|
|
332
251
|
Ok(Some((msg_type, sync_type, binary_string(&response))))
|
|
333
252
|
}
|
|
334
|
-
|
|
335
|
-
/// Encode raw update bytes as a sync Update message
|
|
336
|
-
fn encode_update_message(&self, update: RString) -> RString {
|
|
337
|
-
let update_bytes = copy_bytes(update);
|
|
338
|
-
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
339
|
-
binary_string(&msg.encode_v1())
|
|
340
|
-
}
|
|
341
253
|
}
|
|
342
254
|
|
|
343
255
|
// ============================================================================
|
|
344
|
-
//
|
|
256
|
+
// Protocol codec (stateless) -- exposed as `YrbLite` module functions
|
|
345
257
|
// ============================================================================
|
|
258
|
+
//
|
|
259
|
+
// The server never holds presence or document state to classify a frame; these
|
|
260
|
+
// are pure functions of their bytes. (Presence lives in the browser clients; the
|
|
261
|
+
// server only relays awareness frames opaquely.)
|
|
262
|
+
|
|
263
|
+
/// Wrap a raw document update in a sync Update message frame, ready to relay.
|
|
264
|
+
fn wrap_update(update: RString) -> RString {
|
|
265
|
+
let update_bytes = copy_bytes(update);
|
|
266
|
+
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
267
|
+
binary_string(&msg.encode_v1())
|
|
268
|
+
}
|
|
346
269
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
let awareness = &self.0;
|
|
362
|
-
nogvl(move || awareness.lock().unwrap().doc().client_id().get())
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/// Get the document GUID
|
|
366
|
-
fn guid(&self) -> String {
|
|
367
|
-
let awareness = &self.0;
|
|
368
|
-
nogvl(move || awareness.lock().unwrap().doc().guid().to_string())
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/// A standalone SyncStep1 message (the server's state vector). Sent as its
|
|
372
|
-
/// own frame in the opening handshake so providers that parse one message
|
|
373
|
-
/// per frame (e.g. @y-rb/actioncable) handle it correctly.
|
|
374
|
-
fn sync_step1(&self) -> RString {
|
|
375
|
-
let awareness = &self.0;
|
|
376
|
-
let encoded = nogvl(move || {
|
|
377
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
378
|
-
let txn = doc.transact();
|
|
379
|
-
let sv = txn.state_vector();
|
|
380
|
-
Message::Sync(SyncMessage::SyncStep1(sv)).encode_v1()
|
|
381
|
-
});
|
|
382
|
-
binary_string(&encoded)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/// Create initial sync messages to send when connection opens.
|
|
386
|
-
/// Returns binary data containing SyncStep1 + Awareness update.
|
|
387
|
-
fn start(&self) -> Result<RString, Error> {
|
|
388
|
-
let awareness = &self.0;
|
|
389
|
-
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
390
|
-
let awareness = awareness.lock().unwrap();
|
|
391
|
-
let protocol = DefaultProtocol;
|
|
392
|
-
let mut encoder = EncoderV1::new();
|
|
393
|
-
protocol
|
|
394
|
-
.start(&awareness, &mut encoder)
|
|
395
|
-
.map_err(|e| e.to_string())?;
|
|
396
|
-
Ok(encoder.to_vec())
|
|
397
|
-
})
|
|
398
|
-
.map_err(yrb_error)?;
|
|
399
|
-
Ok(binary_string(&encoded))
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/// Handle incoming message and return response messages (if any).
|
|
403
|
-
/// Returns binary data containing response messages, or empty if no response needed.
|
|
404
|
-
fn handle(&self, data: RString) -> Result<RString, Error> {
|
|
405
|
-
let data_bytes = copy_bytes(data);
|
|
406
|
-
let awareness = &self.0;
|
|
407
|
-
|
|
408
|
-
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
409
|
-
validate_frame_client_ids(&data_bytes)?;
|
|
410
|
-
let mut awareness = awareness.lock().unwrap();
|
|
411
|
-
let protocol = DefaultProtocol;
|
|
412
|
-
let responses = protocol
|
|
413
|
-
.handle(&mut awareness, &data_bytes)
|
|
414
|
-
.map_err(|e| e.to_string())?;
|
|
415
|
-
|
|
416
|
-
if responses.is_empty() {
|
|
417
|
-
return Ok(Vec::new());
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
let mut encoder = EncoderV1::new();
|
|
421
|
-
for msg in responses {
|
|
422
|
-
msg.encode(&mut encoder);
|
|
423
|
-
}
|
|
424
|
-
Ok(encoder.to_vec())
|
|
425
|
-
})
|
|
426
|
-
.map_err(yrb_error)?;
|
|
427
|
-
Ok(binary_string(&encoded))
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/// Encode an update message for broadcasting changes to peers.
|
|
431
|
-
fn encode_update(&self, update: RString) -> Result<RString, Error> {
|
|
432
|
-
let update_bytes = copy_bytes(update);
|
|
433
|
-
nogvl({
|
|
434
|
-
let update_bytes = update_bytes.clone();
|
|
435
|
-
move || validate_update_client_ids(&update_bytes)
|
|
436
|
-
})
|
|
437
|
-
.map_err(yrb_error)?;
|
|
438
|
-
let msg = Message::Sync(SyncMessage::Update(update_bytes));
|
|
439
|
-
Ok(binary_string(&msg.encode_v1()))
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/// Get the current state vector encoded as bytes
|
|
443
|
-
fn encode_state_vector(&self) -> RString {
|
|
444
|
-
let awareness = &self.0;
|
|
445
|
-
let sv = nogvl(move || {
|
|
446
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
447
|
-
let txn = doc.transact();
|
|
448
|
-
txn.state_vector().encode_v1()
|
|
449
|
-
});
|
|
450
|
-
binary_string(&sv)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/// Encode state as update (optionally diffed against a state vector)
|
|
454
|
-
fn encode_state_as_update(&self, args: &[Value]) -> Result<RString, Error> {
|
|
455
|
-
let sv_bytes: Option<Vec<u8>> = if args.is_empty() {
|
|
456
|
-
None
|
|
457
|
-
} else {
|
|
458
|
-
let sv_string: RString = TryConvert::try_convert(args[0])?;
|
|
459
|
-
Some(copy_bytes(sv_string))
|
|
460
|
-
};
|
|
461
|
-
let awareness = &self.0;
|
|
462
|
-
let update = nogvl(move || -> Result<Vec<u8>, String> {
|
|
463
|
-
let sv = match &sv_bytes {
|
|
464
|
-
None => yrs::StateVector::default(),
|
|
465
|
-
Some(bytes) => yrs::StateVector::decode_v1(bytes).map_err(|e| e.to_string())?,
|
|
466
|
-
};
|
|
467
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
468
|
-
let txn = doc.transact();
|
|
469
|
-
Ok(txn.encode_state_as_update_v1(&sv))
|
|
470
|
-
})
|
|
471
|
-
.map_err(yrb_error)?;
|
|
472
|
-
Ok(binary_string(&update))
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/// Set local awareness state (JSON string)
|
|
476
|
-
fn set_local_state(&self, json: String) -> Result<(), Error> {
|
|
477
|
-
let value: serde_json::Value =
|
|
478
|
-
serde_json::from_str(&json).map_err(|e| yrb_error(e.to_string()))?;
|
|
479
|
-
let awareness = &self.0;
|
|
480
|
-
nogvl(move || -> Result<(), String> {
|
|
481
|
-
awareness
|
|
482
|
-
.lock()
|
|
483
|
-
.unwrap()
|
|
484
|
-
.set_local_state(value)
|
|
485
|
-
.map_err(|e| e.to_string())
|
|
486
|
-
})
|
|
487
|
-
.map_err(yrb_error)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/// Get local awareness state as JSON string (or nil if not set)
|
|
491
|
-
fn local_state(&self) -> Option<String> {
|
|
492
|
-
let awareness = &self.0;
|
|
493
|
-
nogvl(move || {
|
|
494
|
-
awareness
|
|
495
|
-
.lock()
|
|
496
|
-
.unwrap()
|
|
497
|
-
.local_state::<serde_json::Value>()
|
|
498
|
-
.map(|v| v.to_string())
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/// Clear local awareness state
|
|
503
|
-
fn clear_local_state(&self) {
|
|
504
|
-
let awareness = &self.0;
|
|
505
|
-
nogvl(move || awareness.lock().unwrap().clean_local_state());
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/// Get awareness update for broadcasting to peers
|
|
509
|
-
fn encode_awareness_update(&self) -> Result<RString, Error> {
|
|
510
|
-
let awareness = &self.0;
|
|
511
|
-
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
512
|
-
let awareness = awareness.lock().unwrap();
|
|
513
|
-
let update = awareness.update().map_err(|e| e.to_string())?;
|
|
514
|
-
Ok(Message::Awareness(update).encode_v1())
|
|
515
|
-
})
|
|
516
|
-
.map_err(yrb_error)?;
|
|
517
|
-
Ok(binary_string(&encoded))
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/// Apply a raw update to the underlying document
|
|
521
|
-
fn apply_update(&self, update: RString) -> Result<(), Error> {
|
|
522
|
-
let update_bytes = copy_bytes(update);
|
|
523
|
-
let awareness = &self.0;
|
|
524
|
-
nogvl(move || -> Result<(), String> {
|
|
525
|
-
validate_update_client_ids(&update_bytes)?;
|
|
526
|
-
let update = yrs::Update::decode_v1(&update_bytes).map_err(|e| e.to_string())?;
|
|
527
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
528
|
-
let mut txn = doc.transact_mut();
|
|
529
|
-
txn.apply_update(update).map_err(|e| e.to_string())
|
|
530
|
-
})
|
|
531
|
-
.map_err(yrb_error)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/// True if applying `update` would integrate cleanly (its dependencies are
|
|
535
|
-
/// all present). False means it depends on a missing, causally-prior update.
|
|
536
|
-
/// Pure read; does not mutate.
|
|
537
|
-
fn update_ready(&self, update: RString) -> Result<bool, Error> {
|
|
538
|
-
let update_bytes = copy_bytes(update);
|
|
539
|
-
let awareness = &self.0;
|
|
540
|
-
nogvl(move || {
|
|
541
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
542
|
-
update_is_ready(&doc, &update_bytes)
|
|
543
|
-
})
|
|
544
|
-
.map_err(yrb_error)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/// True if applying `update` would change the document, false if it's an
|
|
548
|
-
/// already-applied retry. See `update_advances_doc`. Pure read.
|
|
549
|
-
fn update_advances(&self, update: RString) -> Result<bool, Error> {
|
|
550
|
-
let update_bytes = copy_bytes(update);
|
|
551
|
-
let awareness = &self.0;
|
|
552
|
-
nogvl(move || {
|
|
553
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
554
|
-
update_advances_doc(&doc, &update_bytes)
|
|
555
|
-
})
|
|
556
|
-
.map_err(yrb_error)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/// True if the document holds pending (un-integrable) structs waiting on a
|
|
560
|
-
/// missing dependency.
|
|
561
|
-
fn pending(&self) -> bool {
|
|
562
|
-
let awareness = &self.0;
|
|
563
|
-
nogvl(move || {
|
|
564
|
-
let doc = awareness.lock().unwrap().doc().clone();
|
|
565
|
-
doc_has_pending(&doc)
|
|
566
|
-
})
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/// Decode the awareness client IDs referenced by a protocol message
|
|
570
|
-
/// (which may pack several sub-messages together). Sync sub-messages are
|
|
571
|
-
/// ignored. The ActionCable layer uses this to learn which presence
|
|
572
|
-
/// states arrived on a connection, so it can clear them when that
|
|
573
|
-
/// connection closes.
|
|
574
|
-
fn awareness_client_ids(&self, data: RString) -> Result<Vec<u64>, Error> {
|
|
575
|
-
let data_bytes = copy_bytes(data);
|
|
576
|
-
nogvl(move || awareness_client_ids_in(&data_bytes)).map_err(yrb_error)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/// Classify a frame for safe routing and relay. Returns a code only when
|
|
580
|
-
/// the frame is exactly one well-formed message that consumes the whole
|
|
581
|
-
/// buffer, so a malformed, truncated, multi-message, or trailing-garbage
|
|
582
|
-
/// frame (which a malicious client could craft to disrupt others if
|
|
583
|
-
/// relayed) is rejected up front:
|
|
584
|
-
/// 0 = drop (malformed, multiple, unknown, or empty)
|
|
585
|
-
/// 1 = sync step1 (a request: respond, do not relay)
|
|
586
|
-
/// 2 = sync step2/update (a document change: record/apply/relay)
|
|
587
|
-
/// 3 = awareness (presence: relay)
|
|
588
|
-
/// 4 = awareness query (a request: respond, do not relay)
|
|
589
|
-
fn message_kind(&self, data: RString) -> u8 {
|
|
590
|
-
let data_bytes = copy_bytes(data);
|
|
591
|
-
nogvl(move || classify_message(&data_bytes))
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/// Extract the document-update delta carried by a protocol message: the
|
|
595
|
-
/// payloads of any Update or SyncStep2 sub-messages, merged into a single
|
|
596
|
-
/// update. Returns nil if the message carries no document change (for
|
|
597
|
-
/// instance a SyncStep1 request or an awareness update). The strict audit
|
|
598
|
-
/// path records this exact delta before applying it.
|
|
599
|
-
fn update_from_message(&self, data: RString) -> Result<Option<RString>, Error> {
|
|
600
|
-
let data_bytes = copy_bytes(data);
|
|
601
|
-
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(yrb_error)?;
|
|
602
|
-
Ok(merged.map(|b| binary_string(&b)))
|
|
603
|
-
}
|
|
270
|
+
/// Classify a frame for safe routing and relay. Returns a code only when the
|
|
271
|
+
/// frame is exactly one well-formed message that consumes the whole buffer, so
|
|
272
|
+
/// a malformed, truncated, multi-message, or trailing-garbage frame (which a
|
|
273
|
+
/// malicious client could craft to disrupt others if relayed) is rejected up
|
|
274
|
+
/// front:
|
|
275
|
+
/// 0 = drop (malformed, multiple, unknown, or empty)
|
|
276
|
+
/// 1 = sync step1 (a request: respond, do not relay)
|
|
277
|
+
/// 2 = sync step2/update (a document change: record/apply/relay)
|
|
278
|
+
/// 3 = awareness (presence: relay)
|
|
279
|
+
/// 4 = awareness query (a request: respond, do not relay)
|
|
280
|
+
fn message_kind(data: RString) -> u8 {
|
|
281
|
+
let data_bytes = copy_bytes(data);
|
|
282
|
+
nogvl(move || classify_message(&data_bytes))
|
|
283
|
+
}
|
|
604
284
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
.map(validate_client_id)
|
|
614
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
615
|
-
let awareness = &self.0;
|
|
616
|
-
let encoded = nogvl(move || -> Result<Vec<u8>, String> {
|
|
617
|
-
let mut awareness = awareness.lock().unwrap();
|
|
618
|
-
let mut removed = Vec::new();
|
|
619
|
-
for id in client_ids {
|
|
620
|
-
let cid = ClientID::new(id);
|
|
621
|
-
if awareness.meta(cid).is_some() {
|
|
622
|
-
awareness.remove_state(cid);
|
|
623
|
-
removed.push(cid);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
if removed.is_empty() {
|
|
627
|
-
return Ok(Vec::new());
|
|
628
|
-
}
|
|
629
|
-
let update = awareness
|
|
630
|
-
.update_with_clients(removed)
|
|
631
|
-
.map_err(|e| e.to_string())?;
|
|
632
|
-
Ok(Message::Awareness(update).encode_v1())
|
|
633
|
-
})
|
|
634
|
-
.map_err(yrb_error)?;
|
|
635
|
-
Ok(binary_string(&encoded))
|
|
636
|
-
}
|
|
285
|
+
/// Extract the document-update delta carried by a protocol message: the payloads
|
|
286
|
+
/// of any Update or SyncStep2 sub-messages, merged into a single update. Returns
|
|
287
|
+
/// nil if the message carries no document change (a SyncStep1 request or an
|
|
288
|
+
/// awareness update). The store-backed path records this exact delta before relay.
|
|
289
|
+
fn update_from_message(data: RString) -> Result<Option<RString>, Error> {
|
|
290
|
+
let data_bytes = copy_bytes(data);
|
|
291
|
+
let merged = nogvl(move || merged_doc_update(&data_bytes)).map_err(yrb_error)?;
|
|
292
|
+
Ok(merged.map(|b| binary_string(&b)))
|
|
637
293
|
}
|
|
638
294
|
|
|
639
295
|
// ============================================================================
|
|
@@ -651,8 +307,6 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
651
307
|
// Define Doc class
|
|
652
308
|
let doc_class = module.define_class("Doc", ruby.class_object())?;
|
|
653
309
|
doc_class.define_singleton_method("new", function!(RbDoc::new, -1))?;
|
|
654
|
-
doc_class.define_method("client_id", method!(RbDoc::client_id, 0))?;
|
|
655
|
-
doc_class.define_method("guid", method!(RbDoc::guid, 0))?;
|
|
656
310
|
doc_class.define_method(
|
|
657
311
|
"encode_state_vector",
|
|
658
312
|
method!(RbDoc::encode_state_vector, 0),
|
|
@@ -664,59 +318,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
664
318
|
doc_class.define_method("apply_update", method!(RbDoc::apply_update, 1))?;
|
|
665
319
|
doc_class.define_method("update_ready?", method!(RbDoc::update_ready, 1))?;
|
|
666
320
|
doc_class.define_method("update_advances?", method!(RbDoc::update_advances, 1))?;
|
|
667
|
-
doc_class.define_method("pending?", method!(RbDoc::pending, 0))?;
|
|
668
321
|
doc_class.define_method("sync_step1", method!(RbDoc::sync_step1, 0))?;
|
|
669
|
-
doc_class.define_method("sync_step2", method!(RbDoc::sync_step2, 1))?;
|
|
670
322
|
doc_class.define_method(
|
|
671
323
|
"handle_sync_message",
|
|
672
324
|
method!(RbDoc::handle_sync_message, 1),
|
|
673
325
|
)?;
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
)?;
|
|
678
|
-
|
|
679
|
-
// Define Awareness class
|
|
680
|
-
let awareness_class = module.define_class("Awareness", ruby.class_object())?;
|
|
681
|
-
awareness_class.define_singleton_method("new", function!(RbAwareness::new, -1))?;
|
|
682
|
-
awareness_class.define_method("client_id", method!(RbAwareness::client_id, 0))?;
|
|
683
|
-
awareness_class.define_method("guid", method!(RbAwareness::guid, 0))?;
|
|
684
|
-
awareness_class.define_method("start", method!(RbAwareness::start, 0))?;
|
|
685
|
-
awareness_class.define_method("sync_step1", method!(RbAwareness::sync_step1, 0))?;
|
|
686
|
-
awareness_class.define_method("handle", method!(RbAwareness::handle, 1))?;
|
|
687
|
-
awareness_class.define_method("encode_update", method!(RbAwareness::encode_update, 1))?;
|
|
688
|
-
awareness_class.define_method(
|
|
689
|
-
"encode_state_vector",
|
|
690
|
-
method!(RbAwareness::encode_state_vector, 0),
|
|
691
|
-
)?;
|
|
692
|
-
awareness_class.define_method(
|
|
693
|
-
"encode_state_as_update",
|
|
694
|
-
method!(RbAwareness::encode_state_as_update, -1),
|
|
695
|
-
)?;
|
|
696
|
-
awareness_class.define_method("apply_update", method!(RbAwareness::apply_update, 1))?;
|
|
697
|
-
awareness_class.define_method("update_ready?", method!(RbAwareness::update_ready, 1))?;
|
|
698
|
-
awareness_class.define_method("update_advances?", method!(RbAwareness::update_advances, 1))?;
|
|
699
|
-
awareness_class.define_method("pending?", method!(RbAwareness::pending, 0))?;
|
|
700
|
-
awareness_class.define_method("set_local_state", method!(RbAwareness::set_local_state, 1))?;
|
|
701
|
-
awareness_class.define_method("local_state", method!(RbAwareness::local_state, 0))?;
|
|
702
|
-
awareness_class.define_method(
|
|
703
|
-
"clear_local_state",
|
|
704
|
-
method!(RbAwareness::clear_local_state, 0),
|
|
705
|
-
)?;
|
|
706
|
-
awareness_class.define_method(
|
|
707
|
-
"encode_awareness_update",
|
|
708
|
-
method!(RbAwareness::encode_awareness_update, 0),
|
|
709
|
-
)?;
|
|
710
|
-
awareness_class.define_method(
|
|
711
|
-
"awareness_client_ids",
|
|
712
|
-
method!(RbAwareness::awareness_client_ids, 1),
|
|
713
|
-
)?;
|
|
714
|
-
awareness_class.define_method("remove_clients", method!(RbAwareness::remove_clients, 1))?;
|
|
715
|
-
awareness_class.define_method(
|
|
716
|
-
"update_from_message",
|
|
717
|
-
method!(RbAwareness::update_from_message, 1),
|
|
718
|
-
)?;
|
|
719
|
-
awareness_class.define_method("message_kind", method!(RbAwareness::message_kind, 1))?;
|
|
326
|
+
// Stateless protocol codec, as YrbLite module functions.
|
|
327
|
+
module.define_module_function("wrap_update", function!(wrap_update, 1))?;
|
|
328
|
+
module.define_module_function("message_kind", function!(message_kind, 1))?;
|
|
329
|
+
module.define_module_function("update_from_message", function!(update_from_message, 1))?;
|
|
720
330
|
|
|
721
331
|
// Define message type constants
|
|
722
332
|
module.const_set("MSG_SYNC", 0u8)?;
|
|
@@ -1,219 +1,18 @@
|
|
|
1
|
-
// Pure protocol helpers
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
use yrs::encoding::read::{Cursor,
|
|
8
|
-
use yrs::sync::protocol::
|
|
9
|
-
MessageReader, MSG_AUTH, MSG_AWARENESS, MSG_QUERY_AWARENESS, MSG_SYNC, MSG_SYNC_STEP_1,
|
|
10
|
-
MSG_SYNC_STEP_2, MSG_SYNC_UPDATE, PERMISSION_DENIED,
|
|
11
|
-
};
|
|
1
|
+
// Pure rust protocol helpers (ie, no Ruby interop).
|
|
2
|
+
//
|
|
3
|
+
// CLIENT IDs ARE NOT VALIDATED HERE -- on purpose. We deliberately don't police it -- every
|
|
4
|
+
// legitimate peer (browser Yjs, and yrs's own `ClientID::random`) already emits
|
|
5
|
+
// 53-bit ids, so it's the client's responsibility not to send a bad one, and we
|
|
6
|
+
// don't want to own that logic.
|
|
7
|
+
use yrs::encoding::read::{Cursor, Read};
|
|
8
|
+
use yrs::sync::protocol::MessageReader;
|
|
12
9
|
use yrs::sync::{Message, SyncMessage};
|
|
13
|
-
use yrs::updates::decoder::{Decode,
|
|
14
|
-
use yrs::{
|
|
15
|
-
|
|
16
|
-
fn unsafe_client_id_error(id: u64) -> ReadError {
|
|
17
|
-
ReadError::Custom(format!(
|
|
18
|
-
"client_id {id} exceeds the maximum safe integer (2^53 - 1); \
|
|
19
|
-
Yjs client IDs must be JS-safe integers to avoid collisions"
|
|
20
|
-
))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
fn checked_client_id(id: u64) -> Result<ClientID, ReadError> {
|
|
24
|
-
if !is_safe_client_id(id) {
|
|
25
|
-
return Err(unsafe_client_id_error(id));
|
|
26
|
-
}
|
|
27
|
-
Ok(ClientID::new(id))
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
fn validate_raw_client_id(id: u64) -> Result<(), ReadError> {
|
|
31
|
-
if !is_safe_client_id(id) {
|
|
32
|
-
return Err(unsafe_client_id_error(id));
|
|
33
|
-
}
|
|
34
|
-
Ok(())
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
struct CheckedDecoderV1<'a> {
|
|
38
|
-
cursor: Cursor<'a>,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
impl<'a> CheckedDecoderV1<'a> {
|
|
42
|
-
fn new(cursor: Cursor<'a>) -> Self {
|
|
43
|
-
CheckedDecoderV1 { cursor }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
fn read_id(&mut self) -> Result<ID, ReadError> {
|
|
47
|
-
let client: u64 = self.read_var()?;
|
|
48
|
-
validate_raw_client_id(client)?;
|
|
49
|
-
let clock = self.read_var()?;
|
|
50
|
-
Ok(ID::new(ClientID::new(client), clock))
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
impl<'a> Read for CheckedDecoderV1<'a> {
|
|
55
|
-
#[inline]
|
|
56
|
-
fn read_u8(&mut self) -> Result<u8, ReadError> {
|
|
57
|
-
self.cursor.read_u8()
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[inline]
|
|
61
|
-
fn read_exact(&mut self, len: usize) -> Result<&[u8], ReadError> {
|
|
62
|
-
self.cursor.read_exact(len)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
impl<'a> Decoder for CheckedDecoderV1<'a> {
|
|
67
|
-
#[inline]
|
|
68
|
-
fn reset_ds_cur_val(&mut self) {}
|
|
69
|
-
|
|
70
|
-
#[inline]
|
|
71
|
-
fn read_ds_clock(&mut self) -> Result<u32, ReadError> {
|
|
72
|
-
self.read_var()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#[inline]
|
|
76
|
-
fn read_ds_len(&mut self) -> Result<u32, ReadError> {
|
|
77
|
-
self.read_var()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
#[inline]
|
|
81
|
-
fn read_left_id(&mut self) -> Result<ID, ReadError> {
|
|
82
|
-
self.read_id()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
#[inline]
|
|
86
|
-
fn read_right_id(&mut self) -> Result<ID, ReadError> {
|
|
87
|
-
self.read_id()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#[inline]
|
|
91
|
-
fn read_client(&mut self) -> Result<ClientID, ReadError> {
|
|
92
|
-
let client: u64 = self.cursor.read_var()?;
|
|
93
|
-
checked_client_id(client)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
#[inline]
|
|
97
|
-
fn read_info(&mut self) -> Result<u8, ReadError> {
|
|
98
|
-
self.cursor.read_u8()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
#[inline]
|
|
102
|
-
fn read_parent_info(&mut self) -> Result<bool, ReadError> {
|
|
103
|
-
let info: u32 = self.cursor.read_var()?;
|
|
104
|
-
Ok(info == 1)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
#[inline]
|
|
108
|
-
fn read_type_ref(&mut self) -> Result<u8, ReadError> {
|
|
109
|
-
self.cursor.read_u8()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
#[inline]
|
|
113
|
-
fn read_len(&mut self) -> Result<u32, ReadError> {
|
|
114
|
-
self.read_var()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#[inline]
|
|
118
|
-
fn read_any(&mut self) -> Result<Any, ReadError> {
|
|
119
|
-
Any::decode(self)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#[inline]
|
|
123
|
-
fn read_json(&mut self) -> Result<Any, ReadError> {
|
|
124
|
-
let src = self.read_string()?;
|
|
125
|
-
Any::from_json(src)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
#[inline]
|
|
129
|
-
fn read_key(&mut self) -> Result<Arc<str>, ReadError> {
|
|
130
|
-
let str: Arc<str> = self.read_string()?.into();
|
|
131
|
-
Ok(str)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
#[inline]
|
|
135
|
-
fn read_to_end(&mut self) -> Result<&[u8], ReadError> {
|
|
136
|
-
Ok(&self.cursor.buf[self.cursor.next..])
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
pub(crate) fn validate_state_vector_client_ids(bytes: &[u8]) -> Result<(), String> {
|
|
141
|
-
let mut cursor = Cursor::new(bytes);
|
|
142
|
-
let len: u32 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
143
|
-
for _ in 0..len {
|
|
144
|
-
let client: u64 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
145
|
-
validate_raw_client_id(client).map_err(|e| e.to_string())?;
|
|
146
|
-
let _: u32 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
147
|
-
}
|
|
148
|
-
if cursor.has_content() {
|
|
149
|
-
return Err("state vector has trailing bytes".to_string());
|
|
150
|
-
}
|
|
151
|
-
Ok(())
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
pub(crate) fn validate_update_client_ids(update_bytes: &[u8]) -> Result<(), String> {
|
|
155
|
-
let mut decoder = CheckedDecoderV1::new(Cursor::new(update_bytes));
|
|
156
|
-
yrs::Update::decode(&mut decoder).map_err(|e| e.to_string())?;
|
|
157
|
-
if decoder.cursor.has_content() {
|
|
158
|
-
return Err("update has trailing bytes".to_string());
|
|
159
|
-
}
|
|
160
|
-
Ok(())
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
fn validate_awareness_update_client_ids(bytes: &[u8]) -> Result<(), String> {
|
|
164
|
-
let mut cursor = Cursor::new(bytes);
|
|
165
|
-
let len: u32 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
166
|
-
for _ in 0..len {
|
|
167
|
-
let client: u64 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
168
|
-
validate_raw_client_id(client).map_err(|e| e.to_string())?;
|
|
169
|
-
let _: u32 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
170
|
-
let _ = cursor.read_string().map_err(|e| e.to_string())?;
|
|
171
|
-
}
|
|
172
|
-
if cursor.has_content() {
|
|
173
|
-
return Err("awareness update has trailing bytes".to_string());
|
|
174
|
-
}
|
|
175
|
-
Ok(())
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
pub(crate) fn validate_frame_client_ids(bytes: &[u8]) -> Result<(), String> {
|
|
179
|
-
let mut cursor = Cursor::new(bytes);
|
|
180
|
-
while cursor.has_content() {
|
|
181
|
-
let tag: u8 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
182
|
-
match tag {
|
|
183
|
-
MSG_SYNC => {
|
|
184
|
-
let sync_tag: u8 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
185
|
-
let payload = cursor.read_buf().map_err(|e| e.to_string())?;
|
|
186
|
-
match sync_tag {
|
|
187
|
-
MSG_SYNC_STEP_1 => validate_state_vector_client_ids(payload)?,
|
|
188
|
-
MSG_SYNC_STEP_2 | MSG_SYNC_UPDATE => validate_update_client_ids(payload)?,
|
|
189
|
-
_ => return Err("unknown sync message type".to_string()),
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
MSG_AWARENESS => {
|
|
193
|
-
let payload = cursor.read_buf().map_err(|e| e.to_string())?;
|
|
194
|
-
validate_awareness_update_client_ids(payload)?;
|
|
195
|
-
}
|
|
196
|
-
MSG_AUTH => {
|
|
197
|
-
let permission: u8 = cursor.read_var().map_err(|e| e.to_string())?;
|
|
198
|
-
if permission == PERMISSION_DENIED {
|
|
199
|
-
let _ = cursor.read_string().map_err(|e| e.to_string())?;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
MSG_QUERY_AWARENESS => {}
|
|
203
|
-
_ => {
|
|
204
|
-
let _ = cursor.read_buf().map_err(|e| e.to_string())?;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
Ok(())
|
|
209
|
-
}
|
|
10
|
+
use yrs::updates::decoder::{Decode, DecoderV1};
|
|
11
|
+
use yrs::{Doc, ReadTxn, Transact};
|
|
210
12
|
|
|
211
13
|
/// Classify a frame: a non-zero code only for exactly one well-formed message
|
|
212
14
|
/// that consumes the whole buffer (see `RbAwareness::message_kind` for codes).
|
|
213
15
|
pub(crate) fn classify_message(bytes: &[u8]) -> u8 {
|
|
214
|
-
if validate_frame_client_ids(bytes).is_err() {
|
|
215
|
-
return 0;
|
|
216
|
-
}
|
|
217
16
|
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
218
17
|
let msg = match Message::decode(&mut decoder) {
|
|
219
18
|
Ok(msg) => msg,
|
|
@@ -236,7 +35,6 @@ pub(crate) fn classify_message(bytes: &[u8]) -> u8 {
|
|
|
236
35
|
/// frame into one update, or `None` if the frame carries no document change
|
|
237
36
|
/// (a request, an awareness update, or a no-op handshake SyncStep2).
|
|
238
37
|
pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String> {
|
|
239
|
-
validate_frame_client_ids(bytes)?;
|
|
240
38
|
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
241
39
|
let mut updates: Vec<Vec<u8>> = Vec::new();
|
|
242
40
|
for msg in MessageReader::new(&mut decoder) {
|
|
@@ -255,7 +53,7 @@ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String>
|
|
|
255
53
|
let update = yrs::Update::decode_v1(&merged).map_err(|e| e.to_string())?;
|
|
256
54
|
// A genuine no-op (e.g. the empty SyncStep2 in an opening handshake) carries
|
|
257
55
|
// no structs, no deletes, and no dependencies. We must NOT treat a causally-
|
|
258
|
-
// pending update as a no-op:
|
|
56
|
+
// pending update as a no-op: such an update reports an empty
|
|
259
57
|
// state_vector (its structs can't integrate yet), but it still carries
|
|
260
58
|
// content and a non-empty lower bound (the deps it's waiting on). Dropping it
|
|
261
59
|
// here would silently swallow a gappy update instead of rejecting + resyncing.
|
|
@@ -268,26 +66,12 @@ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String>
|
|
|
268
66
|
Ok(Some(merged))
|
|
269
67
|
}
|
|
270
68
|
|
|
271
|
-
/// Collect the awareness client IDs referenced by a frame's awareness messages.
|
|
272
|
-
pub(crate) fn awareness_client_ids_in(bytes: &[u8]) -> Result<Vec<u64>, String> {
|
|
273
|
-
validate_frame_client_ids(bytes)?;
|
|
274
|
-
let mut decoder = DecoderV1::new(Cursor::new(bytes));
|
|
275
|
-
let mut ids = Vec::new();
|
|
276
|
-
for msg in MessageReader::new(&mut decoder) {
|
|
277
|
-
if let Message::Awareness(update) = msg.map_err(|e| e.to_string())? {
|
|
278
|
-
ids.extend(update.clients.keys().map(|c| c.get()));
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
Ok(ids)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
69
|
/// True if applying `update_bytes` to `doc` would integrate cleanly: every
|
|
285
70
|
/// dependency the update references is already present (the doc's state vector
|
|
286
71
|
/// covers the update's lower bound). A pure read; does not mutate the doc.
|
|
287
72
|
/// When false, applying it would park a pending struct -- the signal that an
|
|
288
73
|
/// earlier, causally-prior update is missing.
|
|
289
74
|
pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
|
|
290
|
-
validate_update_client_ids(update_bytes)?;
|
|
291
75
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
292
76
|
Ok(doc.transact().state_vector() >= update.state_vector_lower())
|
|
293
77
|
}
|
|
@@ -309,7 +93,6 @@ pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, St
|
|
|
309
93
|
/// double-record a pure-delete retry, but it NEVER drops a real deletion, which
|
|
310
94
|
/// is the safe direction. Assumes the update is already causally ready.
|
|
311
95
|
pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
|
|
312
|
-
validate_update_client_ids(update_bytes)?;
|
|
313
96
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
314
97
|
if !update.delete_set().is_empty() {
|
|
315
98
|
return Ok(true); // can't cheaply prove a delete is a duplicate; record it
|
|
@@ -332,8 +115,9 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
332
115
|
}
|
|
333
116
|
|
|
334
117
|
/// True if the doc holds pending structs or a pending delete set -- blocks that
|
|
335
|
-
/// couldn't integrate because a dependency is missing.
|
|
336
|
-
///
|
|
118
|
+
/// couldn't integrate because a dependency is missing. Test-only: asserts the
|
|
119
|
+
/// causal-chain parking behavior in the unit tests below.
|
|
120
|
+
#[cfg(test)]
|
|
337
121
|
pub(crate) fn doc_has_pending(doc: &Doc) -> bool {
|
|
338
122
|
let txn = doc.transact();
|
|
339
123
|
txn.store().pending_update().is_some() || txn.store().pending_ds().is_some()
|
|
@@ -342,10 +126,8 @@ pub(crate) fn doc_has_pending(doc: &Doc) -> bool {
|
|
|
342
126
|
#[cfg(test)]
|
|
343
127
|
mod tests {
|
|
344
128
|
use super::*;
|
|
345
|
-
use crate::is_safe_client_id;
|
|
346
|
-
use yrs::encoding::write::Write;
|
|
347
129
|
use yrs::sync::Awareness;
|
|
348
|
-
use yrs::updates::encoder::
|
|
130
|
+
use yrs::updates::encoder::Encode;
|
|
349
131
|
use yrs::Text;
|
|
350
132
|
|
|
351
133
|
fn text_update(content: &str) -> Vec<u8> {
|
|
@@ -374,42 +156,6 @@ mod tests {
|
|
|
374
156
|
Message::Awareness(awareness.update().unwrap()).encode_v1()
|
|
375
157
|
}
|
|
376
158
|
|
|
377
|
-
fn unsafe_struct_client_update() -> Vec<u8> {
|
|
378
|
-
let mut update = EncoderV1::new();
|
|
379
|
-
update.write_var(1u32); // client count
|
|
380
|
-
update.write_var(0u32); // block count for this client
|
|
381
|
-
update.write_var(1u64 << 53); // unsafe client id
|
|
382
|
-
update.write_var(0u32); // clock
|
|
383
|
-
update.write_var(0u32); // delete-set client count
|
|
384
|
-
update.to_vec()
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
fn unsafe_awareness_frame() -> Vec<u8> {
|
|
388
|
-
let mut payload = EncoderV1::new();
|
|
389
|
-
payload.write_var(1u32); // client count
|
|
390
|
-
payload.write_var(1u64 << 53); // unsafe client id
|
|
391
|
-
payload.write_var(1u32); // clock
|
|
392
|
-
payload.write_string("{}");
|
|
393
|
-
|
|
394
|
-
let mut frame = EncoderV1::new();
|
|
395
|
-
frame.write_var(MSG_AWARENESS);
|
|
396
|
-
frame.write_buf(payload.to_vec());
|
|
397
|
-
frame.to_vec()
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
fn unsafe_step1_frame() -> Vec<u8> {
|
|
401
|
-
let mut sv = EncoderV1::new();
|
|
402
|
-
sv.write_var(1u32); // state-vector entry count
|
|
403
|
-
sv.write_var(1u64 << 53); // unsafe client id
|
|
404
|
-
sv.write_var(0u32); // clock
|
|
405
|
-
|
|
406
|
-
let mut frame = EncoderV1::new();
|
|
407
|
-
frame.write_var(MSG_SYNC);
|
|
408
|
-
frame.write_var(MSG_SYNC_STEP_1);
|
|
409
|
-
frame.write_buf(sv.to_vec());
|
|
410
|
-
frame.to_vec()
|
|
411
|
-
}
|
|
412
|
-
|
|
413
159
|
#[test]
|
|
414
160
|
fn classify_accepts_clean_single_messages() {
|
|
415
161
|
assert_eq!(classify_message(&step1_frame()), 1);
|
|
@@ -500,51 +246,6 @@ mod tests {
|
|
|
500
246
|
);
|
|
501
247
|
}
|
|
502
248
|
|
|
503
|
-
#[test]
|
|
504
|
-
fn client_id_safe_integer_boundary() {
|
|
505
|
-
assert!(is_safe_client_id(0), "zero is fine");
|
|
506
|
-
assert!(
|
|
507
|
-
is_safe_client_id((1 << 53) - 1),
|
|
508
|
-
"2^53 - 1 is the max safe id"
|
|
509
|
-
);
|
|
510
|
-
assert!(!is_safe_client_id(1 << 53), "2^53 is unsafe");
|
|
511
|
-
assert!(!is_safe_client_id(1 << 63), "2^63 is unsafe");
|
|
512
|
-
assert!(!is_safe_client_id(u64::MAX), "u64::MAX is unsafe");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
#[test]
|
|
516
|
-
fn wire_client_id_validation_rejects_unsafe_sync_update_clients() {
|
|
517
|
-
let update = unsafe_struct_client_update();
|
|
518
|
-
assert!(
|
|
519
|
-
validate_update_client_ids(&update).is_err(),
|
|
520
|
-
"raw unsafe update client id is rejected before yrs can mask it"
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
let frame = Message::Sync(SyncMessage::Update(update)).encode_v1();
|
|
524
|
-
assert_eq!(
|
|
525
|
-
classify_message(&frame),
|
|
526
|
-
0,
|
|
527
|
-
"unsafe sync frame is not relayable"
|
|
528
|
-
);
|
|
529
|
-
assert!(merged_doc_update(&frame).is_err());
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
#[test]
|
|
533
|
-
fn wire_client_id_validation_rejects_unsafe_awareness_and_step1_clients() {
|
|
534
|
-
assert!(
|
|
535
|
-
validate_frame_client_ids(&unsafe_awareness_frame()).is_err(),
|
|
536
|
-
"raw unsafe awareness client id is rejected"
|
|
537
|
-
);
|
|
538
|
-
assert_eq!(classify_message(&unsafe_awareness_frame()), 0);
|
|
539
|
-
assert!(awareness_client_ids_in(&unsafe_awareness_frame()).is_err());
|
|
540
|
-
|
|
541
|
-
assert!(
|
|
542
|
-
validate_frame_client_ids(&unsafe_step1_frame()).is_err(),
|
|
543
|
-
"raw unsafe state-vector client id is rejected"
|
|
544
|
-
);
|
|
545
|
-
assert_eq!(classify_message(&unsafe_step1_frame()), 0);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
249
|
#[test]
|
|
549
250
|
fn merged_doc_update_extracts_and_skips_no_ops() {
|
|
550
251
|
// A document update yields a delta that reconstructs the content.
|
|
@@ -582,18 +283,6 @@ mod tests {
|
|
|
582
283
|
assert!(yrs::Update::decode_v1(&merged).is_ok());
|
|
583
284
|
}
|
|
584
285
|
|
|
585
|
-
#[test]
|
|
586
|
-
fn awareness_client_ids_are_collected() {
|
|
587
|
-
assert_eq!(
|
|
588
|
-
awareness_client_ids_in(&awareness_frame(111)).unwrap(),
|
|
589
|
-
vec![111]
|
|
590
|
-
);
|
|
591
|
-
// A document frame has no awareness client ids.
|
|
592
|
-
assert!(awareness_client_ids_in(&update_frame("x"))
|
|
593
|
-
.unwrap()
|
|
594
|
-
.is_empty());
|
|
595
|
-
}
|
|
596
|
-
|
|
597
286
|
#[test]
|
|
598
287
|
fn update_readiness_and_pending_detect_a_causal_gap() {
|
|
599
288
|
// Three sequential single-char inserts from one client: A, then B, then
|
data/lib/yrb_lite/version.rb
CHANGED