yrby 0.3.0 → 0.3.1
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 +39 -0
- data/Cargo.lock +644 -0
- data/README.md +15 -0
- data/ext/yrby/src/lib.rs +5 -3
- data/ext/yrby/src/protocol.rs +266 -15
- data/ext/yrby/src/read.rs +84 -8
- data/lib/y/version.rb +1 -1
- metadata +2 -4
- data/lib/y/decoder/version.rb +0 -7
- data/lib/y/decoder.rb +0 -66
- data/lib/yrby-decoder.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: addc2f9c08a109299b6c3db7cbf977b9b39194b7d784424b55876e8357897b29
|
|
4
|
+
data.tar.gz: 586d689d080396b5e3c9cf528f49dfbccd5dca831e6e64bbaf6de35216f74143
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3412853b540745a3a1db1134104c7cda42330d952b9d6eb08d8c20512069eae93f762aa77204cb6ecc3961da0c117df6bb66ef4b2aad5b0b81b05beedf3253b7
|
|
7
|
+
data.tar.gz: 6d65ed5a61f737cd904800747c40e74d7b57fb0f64f77b12128533a0cfbf4c86058e13d2b2a33d9d005a7bfd015c31683fb25054236dbb3f28639f867d9a3a69
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1] - 2026-07-01
|
|
10
|
+
|
|
11
|
+
Fixes from a full source review.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`Doc#update_ready?` is now exact.** It previously checked only the
|
|
16
|
+
per-client clock lower bound, but yrs's real integration gate also requires
|
|
17
|
+
every block referenced by an item's origin / right-origin / parent — which
|
|
18
|
+
routinely belong to *other* clients — and post-Skip blocks in a merged update
|
|
19
|
+
sit above the lower bound. An update could pass the clock check yet park as
|
|
20
|
+
pending; downstream, `update_advances?` then misread the parked update as an
|
|
21
|
+
already-applied retry (pending doesn't move a state vector) and the sync
|
|
22
|
+
channel **acked and dropped real content**. `update_ready?` now
|
|
23
|
+
trial-integrates on a throwaway probe seeded with the doc's integrated state
|
|
24
|
+
(the clock check remains as a cheap pre-filter), so a cross-client-origin gap
|
|
25
|
+
is correctly rejected for a resync. `update_advances?` also gained defense in
|
|
26
|
+
depth: an update that would park reports as advancing, never as a duplicate.
|
|
27
|
+
- **`Doc#read_text` could deadlock the process.** It opened a second read
|
|
28
|
+
transaction while still holding the first (a chained temporary); yrs's lock is
|
|
29
|
+
write-preferring, so a concurrent writer between the two acquisitions
|
|
30
|
+
deadlocked reader-vs-writer inside the GVL-released (uninterruptible) region.
|
|
31
|
+
Now uses a single transaction.
|
|
32
|
+
- **TOCTOU in gap-free encoding.** The pending check and the encode ran in
|
|
33
|
+
separate transactions, so a concurrent gappy `apply_update` between them could
|
|
34
|
+
make `handle_sync_message`/`compacted_state_update` serve pending structs
|
|
35
|
+
anyway. Both now happen under one transaction.
|
|
36
|
+
- `read_xml`: Lexical soft line breaks and tabs now come through as `\n`/`\t`
|
|
37
|
+
instead of vanishing (`"foo⏎bar"` no longer extracts as `"foobar"`).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `update_advances?` skips its full-document probe when the update carries
|
|
42
|
+
blocks beyond the doc's state vector (a novel update trivially advances) —
|
|
43
|
+
the common case no longer pays O(doc) per frame.
|
|
44
|
+
- The gem no longer packages the `yrby-decoder` gem's files (they ship in that
|
|
45
|
+
gem; the duplicate copy could shadow a newer standalone release), and now
|
|
46
|
+
ships `Cargo.lock` so source builds compile the exact crate graph CI tested.
|
|
47
|
+
|
|
9
48
|
## [0.3.0] - 2026-07-01
|
|
10
49
|
|
|
11
50
|
### Fixed
|
data/Cargo.lock
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "aho-corasick"
|
|
7
|
+
version = "1.1.4"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"memchr",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "arc-swap"
|
|
16
|
+
version = "1.8.0"
|
|
17
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
+
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"rustversion",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "async-lock"
|
|
25
|
+
version = "3.4.2"
|
|
26
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
|
+
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
|
28
|
+
dependencies = [
|
|
29
|
+
"event-listener",
|
|
30
|
+
"event-listener-strategy",
|
|
31
|
+
"pin-project-lite",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[[package]]
|
|
35
|
+
name = "async-trait"
|
|
36
|
+
version = "0.1.89"
|
|
37
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
38
|
+
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|
39
|
+
dependencies = [
|
|
40
|
+
"proc-macro2",
|
|
41
|
+
"quote",
|
|
42
|
+
"syn",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[[package]]
|
|
46
|
+
name = "bindgen"
|
|
47
|
+
version = "0.69.5"
|
|
48
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
49
|
+
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
|
50
|
+
dependencies = [
|
|
51
|
+
"bitflags",
|
|
52
|
+
"cexpr",
|
|
53
|
+
"clang-sys",
|
|
54
|
+
"itertools",
|
|
55
|
+
"lazy_static",
|
|
56
|
+
"lazycell",
|
|
57
|
+
"proc-macro2",
|
|
58
|
+
"quote",
|
|
59
|
+
"regex",
|
|
60
|
+
"rustc-hash",
|
|
61
|
+
"shlex",
|
|
62
|
+
"syn",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[[package]]
|
|
66
|
+
name = "bitflags"
|
|
67
|
+
version = "2.10.0"
|
|
68
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
69
|
+
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
|
70
|
+
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "bumpalo"
|
|
73
|
+
version = "3.19.1"
|
|
74
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
75
|
+
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
|
76
|
+
|
|
77
|
+
[[package]]
|
|
78
|
+
name = "cexpr"
|
|
79
|
+
version = "0.6.0"
|
|
80
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
81
|
+
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
|
82
|
+
dependencies = [
|
|
83
|
+
"nom",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[[package]]
|
|
87
|
+
name = "cfg-if"
|
|
88
|
+
version = "1.0.4"
|
|
89
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
90
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
91
|
+
|
|
92
|
+
[[package]]
|
|
93
|
+
name = "clang-sys"
|
|
94
|
+
version = "1.8.1"
|
|
95
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
96
|
+
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
|
97
|
+
dependencies = [
|
|
98
|
+
"glob",
|
|
99
|
+
"libc",
|
|
100
|
+
"libloading",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[[package]]
|
|
104
|
+
name = "concurrent-queue"
|
|
105
|
+
version = "2.5.0"
|
|
106
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
107
|
+
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
|
108
|
+
dependencies = [
|
|
109
|
+
"crossbeam-utils",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[[package]]
|
|
113
|
+
name = "crossbeam-utils"
|
|
114
|
+
version = "0.8.21"
|
|
115
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
116
|
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|
117
|
+
|
|
118
|
+
[[package]]
|
|
119
|
+
name = "dashmap"
|
|
120
|
+
version = "6.1.0"
|
|
121
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
122
|
+
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
|
123
|
+
dependencies = [
|
|
124
|
+
"cfg-if",
|
|
125
|
+
"crossbeam-utils",
|
|
126
|
+
"hashbrown",
|
|
127
|
+
"lock_api",
|
|
128
|
+
"once_cell",
|
|
129
|
+
"parking_lot_core",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
[[package]]
|
|
133
|
+
name = "either"
|
|
134
|
+
version = "1.15.0"
|
|
135
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
136
|
+
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
|
137
|
+
|
|
138
|
+
[[package]]
|
|
139
|
+
name = "event-listener"
|
|
140
|
+
version = "5.4.1"
|
|
141
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
142
|
+
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
|
143
|
+
dependencies = [
|
|
144
|
+
"concurrent-queue",
|
|
145
|
+
"parking",
|
|
146
|
+
"pin-project-lite",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
[[package]]
|
|
150
|
+
name = "event-listener-strategy"
|
|
151
|
+
version = "0.5.4"
|
|
152
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
153
|
+
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
|
154
|
+
dependencies = [
|
|
155
|
+
"event-listener",
|
|
156
|
+
"pin-project-lite",
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
[[package]]
|
|
160
|
+
name = "fastrand"
|
|
161
|
+
version = "2.3.0"
|
|
162
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
163
|
+
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
|
164
|
+
dependencies = [
|
|
165
|
+
"getrandom",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
[[package]]
|
|
169
|
+
name = "getrandom"
|
|
170
|
+
version = "0.2.17"
|
|
171
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
172
|
+
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
|
173
|
+
dependencies = [
|
|
174
|
+
"cfg-if",
|
|
175
|
+
"js-sys",
|
|
176
|
+
"libc",
|
|
177
|
+
"wasi",
|
|
178
|
+
"wasm-bindgen",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
[[package]]
|
|
182
|
+
name = "glob"
|
|
183
|
+
version = "0.3.3"
|
|
184
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
185
|
+
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
|
186
|
+
|
|
187
|
+
[[package]]
|
|
188
|
+
name = "hashbrown"
|
|
189
|
+
version = "0.14.5"
|
|
190
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
191
|
+
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
|
192
|
+
|
|
193
|
+
[[package]]
|
|
194
|
+
name = "itertools"
|
|
195
|
+
version = "0.12.1"
|
|
196
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
197
|
+
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
198
|
+
dependencies = [
|
|
199
|
+
"either",
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
[[package]]
|
|
203
|
+
name = "itoa"
|
|
204
|
+
version = "1.0.17"
|
|
205
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
206
|
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
|
207
|
+
|
|
208
|
+
[[package]]
|
|
209
|
+
name = "js-sys"
|
|
210
|
+
version = "0.3.85"
|
|
211
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
+
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
|
213
|
+
dependencies = [
|
|
214
|
+
"once_cell",
|
|
215
|
+
"wasm-bindgen",
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
[[package]]
|
|
219
|
+
name = "lazy_static"
|
|
220
|
+
version = "1.5.0"
|
|
221
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
222
|
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
223
|
+
|
|
224
|
+
[[package]]
|
|
225
|
+
name = "lazycell"
|
|
226
|
+
version = "1.3.0"
|
|
227
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
228
|
+
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|
229
|
+
|
|
230
|
+
[[package]]
|
|
231
|
+
name = "libc"
|
|
232
|
+
version = "0.2.180"
|
|
233
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
234
|
+
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
|
235
|
+
|
|
236
|
+
[[package]]
|
|
237
|
+
name = "libloading"
|
|
238
|
+
version = "0.8.9"
|
|
239
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
240
|
+
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
|
241
|
+
dependencies = [
|
|
242
|
+
"cfg-if",
|
|
243
|
+
"windows-link",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
[[package]]
|
|
247
|
+
name = "lock_api"
|
|
248
|
+
version = "0.4.14"
|
|
249
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
+
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
|
251
|
+
dependencies = [
|
|
252
|
+
"scopeguard",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
[[package]]
|
|
256
|
+
name = "magnus"
|
|
257
|
+
version = "0.8.2"
|
|
258
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
259
|
+
checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
|
|
260
|
+
dependencies = [
|
|
261
|
+
"magnus-macros",
|
|
262
|
+
"rb-sys",
|
|
263
|
+
"rb-sys-env 0.2.3",
|
|
264
|
+
"seq-macro",
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
[[package]]
|
|
268
|
+
name = "magnus-macros"
|
|
269
|
+
version = "0.8.0"
|
|
270
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
271
|
+
checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
|
|
272
|
+
dependencies = [
|
|
273
|
+
"proc-macro2",
|
|
274
|
+
"quote",
|
|
275
|
+
"syn",
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
[[package]]
|
|
279
|
+
name = "memchr"
|
|
280
|
+
version = "2.7.6"
|
|
281
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
282
|
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
|
283
|
+
|
|
284
|
+
[[package]]
|
|
285
|
+
name = "minimal-lexical"
|
|
286
|
+
version = "0.2.1"
|
|
287
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
288
|
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
289
|
+
|
|
290
|
+
[[package]]
|
|
291
|
+
name = "nom"
|
|
292
|
+
version = "7.1.3"
|
|
293
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
294
|
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
295
|
+
dependencies = [
|
|
296
|
+
"memchr",
|
|
297
|
+
"minimal-lexical",
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
[[package]]
|
|
301
|
+
name = "once_cell"
|
|
302
|
+
version = "1.21.3"
|
|
303
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
304
|
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|
305
|
+
|
|
306
|
+
[[package]]
|
|
307
|
+
name = "parking"
|
|
308
|
+
version = "2.2.1"
|
|
309
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
310
|
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
|
311
|
+
|
|
312
|
+
[[package]]
|
|
313
|
+
name = "parking_lot_core"
|
|
314
|
+
version = "0.9.12"
|
|
315
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
316
|
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|
317
|
+
dependencies = [
|
|
318
|
+
"cfg-if",
|
|
319
|
+
"libc",
|
|
320
|
+
"redox_syscall",
|
|
321
|
+
"smallvec",
|
|
322
|
+
"windows-link",
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
[[package]]
|
|
326
|
+
name = "pin-project-lite"
|
|
327
|
+
version = "0.2.16"
|
|
328
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
329
|
+
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
|
330
|
+
|
|
331
|
+
[[package]]
|
|
332
|
+
name = "proc-macro2"
|
|
333
|
+
version = "1.0.105"
|
|
334
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
335
|
+
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
|
336
|
+
dependencies = [
|
|
337
|
+
"unicode-ident",
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
[[package]]
|
|
341
|
+
name = "quote"
|
|
342
|
+
version = "1.0.43"
|
|
343
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
344
|
+
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
|
345
|
+
dependencies = [
|
|
346
|
+
"proc-macro2",
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
[[package]]
|
|
350
|
+
name = "rb-sys"
|
|
351
|
+
version = "0.9.124"
|
|
352
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
353
|
+
checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
|
|
354
|
+
dependencies = [
|
|
355
|
+
"rb-sys-build",
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
[[package]]
|
|
359
|
+
name = "rb-sys-build"
|
|
360
|
+
version = "0.9.124"
|
|
361
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
362
|
+
checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
|
|
363
|
+
dependencies = [
|
|
364
|
+
"bindgen",
|
|
365
|
+
"lazy_static",
|
|
366
|
+
"proc-macro2",
|
|
367
|
+
"quote",
|
|
368
|
+
"regex",
|
|
369
|
+
"shell-words",
|
|
370
|
+
"syn",
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
[[package]]
|
|
374
|
+
name = "rb-sys-env"
|
|
375
|
+
version = "0.1.2"
|
|
376
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
377
|
+
checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
|
|
378
|
+
|
|
379
|
+
[[package]]
|
|
380
|
+
name = "rb-sys-env"
|
|
381
|
+
version = "0.2.3"
|
|
382
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
383
|
+
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
|
384
|
+
|
|
385
|
+
[[package]]
|
|
386
|
+
name = "redox_syscall"
|
|
387
|
+
version = "0.5.18"
|
|
388
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
389
|
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
|
390
|
+
dependencies = [
|
|
391
|
+
"bitflags",
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
[[package]]
|
|
395
|
+
name = "regex"
|
|
396
|
+
version = "1.12.2"
|
|
397
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
398
|
+
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
|
399
|
+
dependencies = [
|
|
400
|
+
"aho-corasick",
|
|
401
|
+
"memchr",
|
|
402
|
+
"regex-automata",
|
|
403
|
+
"regex-syntax",
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
[[package]]
|
|
407
|
+
name = "regex-automata"
|
|
408
|
+
version = "0.4.13"
|
|
409
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
410
|
+
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
|
411
|
+
dependencies = [
|
|
412
|
+
"aho-corasick",
|
|
413
|
+
"memchr",
|
|
414
|
+
"regex-syntax",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
[[package]]
|
|
418
|
+
name = "regex-syntax"
|
|
419
|
+
version = "0.8.8"
|
|
420
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
421
|
+
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
|
422
|
+
|
|
423
|
+
[[package]]
|
|
424
|
+
name = "rustc-hash"
|
|
425
|
+
version = "1.1.0"
|
|
426
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
427
|
+
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|
428
|
+
|
|
429
|
+
[[package]]
|
|
430
|
+
name = "rustversion"
|
|
431
|
+
version = "1.0.22"
|
|
432
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
433
|
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
434
|
+
|
|
435
|
+
[[package]]
|
|
436
|
+
name = "scopeguard"
|
|
437
|
+
version = "1.2.0"
|
|
438
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
439
|
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
440
|
+
|
|
441
|
+
[[package]]
|
|
442
|
+
name = "seq-macro"
|
|
443
|
+
version = "0.3.6"
|
|
444
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
445
|
+
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
|
446
|
+
|
|
447
|
+
[[package]]
|
|
448
|
+
name = "serde"
|
|
449
|
+
version = "1.0.228"
|
|
450
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
451
|
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
452
|
+
dependencies = [
|
|
453
|
+
"serde_core",
|
|
454
|
+
"serde_derive",
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
[[package]]
|
|
458
|
+
name = "serde_core"
|
|
459
|
+
version = "1.0.228"
|
|
460
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
461
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
462
|
+
dependencies = [
|
|
463
|
+
"serde_derive",
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
[[package]]
|
|
467
|
+
name = "serde_derive"
|
|
468
|
+
version = "1.0.228"
|
|
469
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
470
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
471
|
+
dependencies = [
|
|
472
|
+
"proc-macro2",
|
|
473
|
+
"quote",
|
|
474
|
+
"syn",
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
[[package]]
|
|
478
|
+
name = "serde_json"
|
|
479
|
+
version = "1.0.149"
|
|
480
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
481
|
+
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
|
482
|
+
dependencies = [
|
|
483
|
+
"itoa",
|
|
484
|
+
"memchr",
|
|
485
|
+
"serde",
|
|
486
|
+
"serde_core",
|
|
487
|
+
"zmij",
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
[[package]]
|
|
491
|
+
name = "shell-words"
|
|
492
|
+
version = "1.1.1"
|
|
493
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
494
|
+
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
|
495
|
+
|
|
496
|
+
[[package]]
|
|
497
|
+
name = "shlex"
|
|
498
|
+
version = "1.3.0"
|
|
499
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
500
|
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
501
|
+
|
|
502
|
+
[[package]]
|
|
503
|
+
name = "smallstr"
|
|
504
|
+
version = "0.3.1"
|
|
505
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
506
|
+
checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b"
|
|
507
|
+
dependencies = [
|
|
508
|
+
"smallvec",
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
[[package]]
|
|
512
|
+
name = "smallvec"
|
|
513
|
+
version = "1.15.1"
|
|
514
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
515
|
+
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|
516
|
+
|
|
517
|
+
[[package]]
|
|
518
|
+
name = "syn"
|
|
519
|
+
version = "2.0.114"
|
|
520
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
521
|
+
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
|
522
|
+
dependencies = [
|
|
523
|
+
"proc-macro2",
|
|
524
|
+
"quote",
|
|
525
|
+
"unicode-ident",
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
[[package]]
|
|
529
|
+
name = "thiserror"
|
|
530
|
+
version = "2.0.18"
|
|
531
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
532
|
+
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
|
533
|
+
dependencies = [
|
|
534
|
+
"thiserror-impl",
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
[[package]]
|
|
538
|
+
name = "thiserror-impl"
|
|
539
|
+
version = "2.0.18"
|
|
540
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
541
|
+
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|
542
|
+
dependencies = [
|
|
543
|
+
"proc-macro2",
|
|
544
|
+
"quote",
|
|
545
|
+
"syn",
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
[[package]]
|
|
549
|
+
name = "unicode-ident"
|
|
550
|
+
version = "1.0.22"
|
|
551
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
552
|
+
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
|
553
|
+
|
|
554
|
+
[[package]]
|
|
555
|
+
name = "wasi"
|
|
556
|
+
version = "0.11.1+wasi-snapshot-preview1"
|
|
557
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
558
|
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|
559
|
+
|
|
560
|
+
[[package]]
|
|
561
|
+
name = "wasm-bindgen"
|
|
562
|
+
version = "0.2.108"
|
|
563
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
564
|
+
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
|
565
|
+
dependencies = [
|
|
566
|
+
"cfg-if",
|
|
567
|
+
"once_cell",
|
|
568
|
+
"rustversion",
|
|
569
|
+
"wasm-bindgen-macro",
|
|
570
|
+
"wasm-bindgen-shared",
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
[[package]]
|
|
574
|
+
name = "wasm-bindgen-macro"
|
|
575
|
+
version = "0.2.108"
|
|
576
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
577
|
+
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
|
578
|
+
dependencies = [
|
|
579
|
+
"quote",
|
|
580
|
+
"wasm-bindgen-macro-support",
|
|
581
|
+
]
|
|
582
|
+
|
|
583
|
+
[[package]]
|
|
584
|
+
name = "wasm-bindgen-macro-support"
|
|
585
|
+
version = "0.2.108"
|
|
586
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
587
|
+
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
|
588
|
+
dependencies = [
|
|
589
|
+
"bumpalo",
|
|
590
|
+
"proc-macro2",
|
|
591
|
+
"quote",
|
|
592
|
+
"syn",
|
|
593
|
+
"wasm-bindgen-shared",
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
[[package]]
|
|
597
|
+
name = "wasm-bindgen-shared"
|
|
598
|
+
version = "0.2.108"
|
|
599
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
600
|
+
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
|
601
|
+
dependencies = [
|
|
602
|
+
"unicode-ident",
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
[[package]]
|
|
606
|
+
name = "windows-link"
|
|
607
|
+
version = "0.2.1"
|
|
608
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
609
|
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
610
|
+
|
|
611
|
+
[[package]]
|
|
612
|
+
name = "yrby"
|
|
613
|
+
version = "0.1.0"
|
|
614
|
+
dependencies = [
|
|
615
|
+
"magnus",
|
|
616
|
+
"rb-sys",
|
|
617
|
+
"rb-sys-env 0.1.2",
|
|
618
|
+
"serde_json",
|
|
619
|
+
"yrs",
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
[[package]]
|
|
623
|
+
name = "yrs"
|
|
624
|
+
version = "0.27.2"
|
|
625
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
626
|
+
checksum = "ccc6a0094c76c87b1b72ca0579bf7aa00f540957182264e6561cbde707c2ce51"
|
|
627
|
+
dependencies = [
|
|
628
|
+
"arc-swap",
|
|
629
|
+
"async-lock",
|
|
630
|
+
"async-trait",
|
|
631
|
+
"dashmap",
|
|
632
|
+
"fastrand",
|
|
633
|
+
"serde",
|
|
634
|
+
"serde_json",
|
|
635
|
+
"smallstr",
|
|
636
|
+
"smallvec",
|
|
637
|
+
"thiserror",
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
[[package]]
|
|
641
|
+
name = "zmij"
|
|
642
|
+
version = "1.0.14"
|
|
643
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
644
|
+
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
|
data/README.md
CHANGED
|
@@ -242,6 +242,21 @@ servers:
|
|
|
242
242
|
causal dependencies are already in the store (checked against `on_load`); a
|
|
243
243
|
causally-incomplete update triggers a resync instead, so the log always
|
|
244
244
|
rebuilds cleanly.
|
|
245
|
+
- **An unhealable gap is dropped, not resynced forever.** A resync heals a gap
|
|
246
|
+
whose missing dependency is still in flight. But a *permanently*-orphaned update
|
|
247
|
+
(its dependency is gone for good) stays gappy through every resync, and a client
|
|
248
|
+
retransmitting it would loop endlessly (server resyncs → client resends →
|
|
249
|
+
repeat). After `gap_strike_limit` rejections of the same update on one
|
|
250
|
+
connection (default 3, minimum 2), the channel settles it with
|
|
251
|
+
`{ "ack" => id, "dropped" => true }` and drops it instead of resyncing again —
|
|
252
|
+
breaking the loop while never dropping a *healable* gap (those heal within a
|
|
253
|
+
resync or two, and healing frees the strike). The `dropped` flag lets the
|
|
254
|
+
client surface the loss (`yrby-client` reports it via `onError`) instead of
|
|
255
|
+
silently showing synced. Set `gap_strike_limit nil` to disable. Works on both
|
|
256
|
+
transports: plain ActionCable keeps strikes on the channel instance; under
|
|
257
|
+
AnyCable (fresh instance per RPC command) they persist through
|
|
258
|
+
anycable-rails' `state_attr_accessor` (istate), declared automatically when
|
|
259
|
+
anycable-rails is loaded.
|
|
245
260
|
- **`on_change` is at-least-once, and the durable guarantee is that replaying the
|
|
246
261
|
log reconstructs the document.** Every update triggers `on_change` before it's acked or
|
|
247
262
|
broadcast (record-before-distribute). If exactly-once updates matter for you, **you
|
data/ext/yrby/src/lib.rs
CHANGED
|
@@ -160,9 +160,11 @@ impl RbDoc {
|
|
|
160
160
|
fn read_text(&self, name: String) -> Option<String> {
|
|
161
161
|
let doc = &self.0;
|
|
162
162
|
nogvl(move || {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
// Exactly ONE transaction per call. Opening a second while the
|
|
164
|
+
// first is still held deadlocks against a waiting writer — and
|
|
165
|
+
// inside nogvl that hang can't be interrupted.
|
|
166
|
+
let txn = doc.transact();
|
|
167
|
+
txn.get_text(name.as_str()).map(|t| t.get_string(&txn))
|
|
166
168
|
})
|
|
167
169
|
}
|
|
168
170
|
|
data/ext/yrby/src/protocol.rs
CHANGED
|
@@ -66,14 +66,42 @@ pub(crate) fn merged_doc_update(bytes: &[u8]) -> Result<Option<Vec<u8>>, String>
|
|
|
66
66
|
Ok(Some(merged))
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/// True if applying `update_bytes` to `doc` would integrate cleanly
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
///
|
|
69
|
+
/// True if applying `update_bytes` to `doc` would integrate cleanly; false if
|
|
70
|
+
/// it would park as pending (a causally-prior update is missing). A pure read.
|
|
71
|
+
///
|
|
72
|
+
/// This must be EXACT: the sync layer records on "ready" and resyncs on "not
|
|
73
|
+
/// ready", and a parked update that slipped through would look like an
|
|
74
|
+
/// already-applied retry downstream — acked and dropped, losing real content.
|
|
75
|
+
///
|
|
76
|
+
/// Clocks alone can't decide it. An update can satisfy every per-client clock
|
|
77
|
+
/// and still fail to integrate: its items may reference other clients' blocks
|
|
78
|
+
/// (origins/parents), and merged updates hide internal gaps behind Skip blocks.
|
|
79
|
+
/// So the clock lower bound serves only as a cheap definitive REJECT; "ready"
|
|
80
|
+
/// is decided by trial-integrating on a throwaway probe seeded with the doc's
|
|
81
|
+
/// integrated state — ready iff nothing parks.
|
|
74
82
|
pub(crate) fn update_is_ready(doc: &Doc, update_bytes: &[u8]) -> Result<bool, String> {
|
|
75
83
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
76
|
-
|
|
84
|
+
// Partial order: "not covered" includes incomparable — not ready either way.
|
|
85
|
+
let lower_covered = doc.transact().state_vector() >= update.state_vector_lower();
|
|
86
|
+
if !lower_covered {
|
|
87
|
+
return Ok(false);
|
|
88
|
+
}
|
|
89
|
+
// Seed the probe with the doc's INTEGRATED state (gap-free), for two
|
|
90
|
+
// reasons. A lossless seed would replant the doc's own pre-existing
|
|
91
|
+
// pending in the probe, making has_pending true for EVERY update — the
|
|
92
|
+
// verdict must be about this update, not the doc's baggage. And an update
|
|
93
|
+
// whose dependency exists only in that pending buffer is genuinely not
|
|
94
|
+
// ready: recording it would put a gap in the durable log; a resync heals
|
|
95
|
+
// both it and the pending it leans on as one complete delta.
|
|
96
|
+
let seed = integrated_update(doc, &StateVector::default())?;
|
|
97
|
+
let probe = Doc::new();
|
|
98
|
+
{
|
|
99
|
+
let mut txn = probe.transact_mut();
|
|
100
|
+
txn.apply_update(Update::decode_v1(&seed).map_err(|e| e.to_string())?)
|
|
101
|
+
.map_err(|e| e.to_string())?;
|
|
102
|
+
txn.apply_update(update).map_err(|e| e.to_string())?;
|
|
103
|
+
}
|
|
104
|
+
Ok(!has_pending(&probe))
|
|
77
105
|
}
|
|
78
106
|
|
|
79
107
|
/// True if applying `update_bytes` would actually change `doc`, i.e. it carries
|
|
@@ -107,6 +135,16 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
107
135
|
let update = yrs::Update::decode_v1(update_bytes).map_err(|e| e.to_string())?;
|
|
108
136
|
let has_deletes = !update.delete_set().is_empty();
|
|
109
137
|
|
|
138
|
+
// Fast path: blocks beyond the doc's state vector are content the doc
|
|
139
|
+
// lacks — the update advances, no probe needed. The common case (a novel
|
|
140
|
+
// edit) exits here; only retries and ambiguous diffs pay for the probe.
|
|
141
|
+
if !has_deletes {
|
|
142
|
+
let covered = doc.transact().state_vector() >= update.state_vector();
|
|
143
|
+
if !covered {
|
|
144
|
+
return Ok(true);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
// Seed an independent probe with the doc's current state so we can measure the
|
|
111
149
|
// update's effect without mutating the real doc.
|
|
112
150
|
let probe = Doc::new();
|
|
@@ -117,6 +155,10 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
117
155
|
.transact_mut()
|
|
118
156
|
.apply_update(yrs::Update::decode_v1(¤t).map_err(|e| e.to_string())?)
|
|
119
157
|
.map_err(|e| e.to_string())?;
|
|
158
|
+
let pending_before = {
|
|
159
|
+
probe.transact().store().pending_update().is_some()
|
|
160
|
+
|| probe.transact().store().pending_ds().is_some()
|
|
161
|
+
};
|
|
120
162
|
|
|
121
163
|
if has_deletes {
|
|
122
164
|
// Deletes don't move the state vector; compare the full encoded state
|
|
@@ -139,7 +181,20 @@ pub(crate) fn update_advances_doc(doc: &Doc, update_bytes: &[u8]) -> Result<bool
|
|
|
139
181
|
.apply_update(update)
|
|
140
182
|
.map_err(|e| e.to_string())?;
|
|
141
183
|
let after = probe.transact().state_vector();
|
|
142
|
-
|
|
184
|
+
if before != after {
|
|
185
|
+
return Ok(true);
|
|
186
|
+
}
|
|
187
|
+
// An unchanged state vector is ambiguous. Usually it means the doc
|
|
188
|
+
// already had everything in this update (a retry — return false, don't
|
|
189
|
+
// re-record). But it can ALSO mean the update failed to integrate and
|
|
190
|
+
// was stashed as pending, which doesn't move the state vector either.
|
|
191
|
+
// That case is missing content, not a duplicate — returning false would
|
|
192
|
+
// let a caller ack it and drop it. Distinguish the two by whether the
|
|
193
|
+
// probe gained pending. (The sync flow screens gaps out with
|
|
194
|
+
// update_is_ready before calling this; the check guards direct callers.)
|
|
195
|
+
let pending_after = probe.transact().store().pending_update().is_some()
|
|
196
|
+
|| probe.transact().store().pending_ds().is_some();
|
|
197
|
+
Ok(pending_after != pending_before)
|
|
143
198
|
}
|
|
144
199
|
}
|
|
145
200
|
|
|
@@ -164,14 +219,18 @@ pub(crate) fn has_pending(doc: &Doc) -> bool {
|
|
|
164
219
|
/// Non-destructive: the prune happens only on the throwaway copy; `doc` keeps its
|
|
165
220
|
/// pending, so a genuine gap still heals if its missing dependency later arrives.
|
|
166
221
|
pub(crate) fn integrated_update(doc: &Doc, sv: &StateVector) -> Result<Vec<u8>, String> {
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
.
|
|
222
|
+
// Pending check and encode share ONE transaction — with two, a concurrent
|
|
223
|
+
// gappy apply_update could slip between them and the encode would serve
|
|
224
|
+
// the very pending this function exists to exclude.
|
|
225
|
+
let full = {
|
|
226
|
+
let txn = doc.transact();
|
|
227
|
+
let store = txn.store();
|
|
228
|
+
// Nothing pending: the direct encode is already gap-free.
|
|
229
|
+
if store.pending_update().is_none() && store.pending_ds().is_none() {
|
|
230
|
+
return Ok(txn.encode_state_as_update_v1(sv));
|
|
231
|
+
}
|
|
232
|
+
txn.encode_state_as_update_v1(&StateVector::default())
|
|
233
|
+
};
|
|
175
234
|
let clean = Doc::new();
|
|
176
235
|
{
|
|
177
236
|
let mut txn = clean.transact_mut();
|
|
@@ -464,6 +523,146 @@ mod tests {
|
|
|
464
523
|
assert!(!has_pending(&doc), "u2 arrived; u3 integrated");
|
|
465
524
|
}
|
|
466
525
|
|
|
526
|
+
// Build a cross-client-origin gap: client C creates "abc"; client A applies
|
|
527
|
+
// it and types between C's characters, so A's delta references C's blocks as
|
|
528
|
+
// origins. Returns (c_update, a_delta). On a doc missing `c_update`, the
|
|
529
|
+
// per-client clock lower bound of `a_delta` is satisfied (A starts at clock
|
|
530
|
+
// 0) but integration parks — the case a clock-only readiness check misses.
|
|
531
|
+
fn cross_client_origin_gap() -> (Vec<u8>, Vec<u8>) {
|
|
532
|
+
let c = Doc::new();
|
|
533
|
+
let ct = c.get_or_insert_text("t");
|
|
534
|
+
ct.insert(&mut c.transact_mut(), 0, "abc");
|
|
535
|
+
let c_update = c
|
|
536
|
+
.transact()
|
|
537
|
+
.encode_state_as_update_v1(&yrs::StateVector::default());
|
|
538
|
+
|
|
539
|
+
let a = Doc::new();
|
|
540
|
+
a.transact_mut()
|
|
541
|
+
.apply_update(yrs::Update::decode_v1(&c_update).unwrap())
|
|
542
|
+
.unwrap();
|
|
543
|
+
let sv_before = a.transact().state_vector();
|
|
544
|
+
let at = a.get_or_insert_text("t");
|
|
545
|
+
at.insert(&mut a.transact_mut(), 1, "X"); // between C's chars
|
|
546
|
+
let a_delta = a.transact().encode_state_as_update_v1(&sv_before);
|
|
547
|
+
(c_update, a_delta)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn cross_client_origin_gap_is_not_ready() {
|
|
552
|
+
let (c_update, a_delta) = cross_client_origin_gap();
|
|
553
|
+
|
|
554
|
+
// A server that never saw C's content: the clock lower bound passes, but
|
|
555
|
+
// the update can't integrate — it must NOT be ready (previously it was,
|
|
556
|
+
// and the downstream advances? probe then acked-and-dropped it).
|
|
557
|
+
let server = Doc::new();
|
|
558
|
+
assert!(
|
|
559
|
+
!update_is_ready(&server, &a_delta).unwrap(),
|
|
560
|
+
"a delta with unmet cross-client origins is not ready"
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Once the server has C's content, the same delta is ready and advances.
|
|
564
|
+
server
|
|
565
|
+
.transact_mut()
|
|
566
|
+
.apply_update(yrs::Update::decode_v1(&c_update).unwrap())
|
|
567
|
+
.unwrap();
|
|
568
|
+
assert!(update_is_ready(&server, &a_delta).unwrap());
|
|
569
|
+
assert!(update_advances_doc(&server, &a_delta).unwrap());
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
#[test]
|
|
573
|
+
fn merged_update_with_internal_skip_gap_is_not_ready() {
|
|
574
|
+
// Merging u1 and u3 (u2 missing) yields one update with a Skip block; its
|
|
575
|
+
// clock lower bound is u1's start, but the post-Skip blocks can't
|
|
576
|
+
// integrate on a doc that lacks u2.
|
|
577
|
+
let src = Doc::new();
|
|
578
|
+
let txt = src.get_or_insert_text("t");
|
|
579
|
+
let mut deltas: Vec<Vec<u8>> = Vec::new();
|
|
580
|
+
let mut prev = yrs::StateVector::default();
|
|
581
|
+
for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
|
|
582
|
+
txt.insert(&mut src.transact_mut(), i as u32, ch);
|
|
583
|
+
deltas.push(src.transact().encode_state_as_update_v1(&prev));
|
|
584
|
+
prev = src.transact().state_vector();
|
|
585
|
+
}
|
|
586
|
+
let merged = yrs::merge_updates_v1([deltas[0].as_slice(), deltas[2].as_slice()]).unwrap();
|
|
587
|
+
|
|
588
|
+
let server = Doc::new();
|
|
589
|
+
assert!(
|
|
590
|
+
!update_is_ready(&server, &merged).unwrap(),
|
|
591
|
+
"the post-Skip blocks depend on the missing u2"
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#[test]
|
|
596
|
+
fn a_doc_with_legacy_pending_still_accepts_healthy_updates() {
|
|
597
|
+
// Why update_is_ready seeds its probe with the INTEGRATED state: with a
|
|
598
|
+
// lossless seed, the doc's own pre-existing pending would park in the
|
|
599
|
+
// probe and every verdict would come back "not ready" — a server with
|
|
600
|
+
// one legacy gap would reject every healthy keystroke forever.
|
|
601
|
+
let (_first, dependent) = gap_pair();
|
|
602
|
+
let doc = Doc::new();
|
|
603
|
+
doc.transact_mut()
|
|
604
|
+
.apply_update(yrs::Update::decode_v1(&dependent).unwrap())
|
|
605
|
+
.unwrap();
|
|
606
|
+
assert!(has_pending(&doc), "the doc carries a legacy parked gap");
|
|
607
|
+
|
|
608
|
+
// A healthy, self-contained update from an unrelated client.
|
|
609
|
+
let healthy = {
|
|
610
|
+
let d = Doc::new();
|
|
611
|
+
let t = d.get_or_insert_text("other");
|
|
612
|
+
t.insert(&mut d.transact_mut(), 0, "hello");
|
|
613
|
+
let txn = d.transact();
|
|
614
|
+
txn.encode_state_as_update_v1(&yrs::StateVector::default())
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
assert!(
|
|
618
|
+
update_is_ready(&doc, &healthy).unwrap(),
|
|
619
|
+
"legacy pending must not veto unrelated healthy updates"
|
|
620
|
+
);
|
|
621
|
+
assert!(update_advances_doc(&doc, &healthy).unwrap());
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
#[test]
|
|
625
|
+
fn an_update_depending_only_on_pending_content_is_not_ready() {
|
|
626
|
+
// The other half of the integrated-only seed: a dependency that exists
|
|
627
|
+
// solely in the doc's pending buffer doesn't count — recording such an
|
|
628
|
+
// update would put a gap in the durable log. Not ready; resync heals
|
|
629
|
+
// both as one complete delta.
|
|
630
|
+
let src = Doc::new();
|
|
631
|
+
let txt = src.get_or_insert_text("t");
|
|
632
|
+
let mut deltas: Vec<Vec<u8>> = Vec::new();
|
|
633
|
+
let mut prev = yrs::StateVector::default();
|
|
634
|
+
for (i, ch) in ["A", "B", "C"].into_iter().enumerate() {
|
|
635
|
+
txt.insert(&mut src.transact_mut(), i as u32, ch);
|
|
636
|
+
deltas.push(src.transact().encode_state_as_update_v1(&prev));
|
|
637
|
+
prev = src.transact().state_vector();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The doc holds u2 only as PENDING (u1 never arrived); u3 depends on u2.
|
|
641
|
+
let doc = Doc::new();
|
|
642
|
+
doc.transact_mut()
|
|
643
|
+
.apply_update(yrs::Update::decode_v1(&deltas[1]).unwrap())
|
|
644
|
+
.unwrap();
|
|
645
|
+
assert!(has_pending(&doc), "u2 parked without u1");
|
|
646
|
+
|
|
647
|
+
assert!(
|
|
648
|
+
!update_is_ready(&doc, &deltas[2]).unwrap(),
|
|
649
|
+
"a dependency satisfied only by pending content is not ready"
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#[test]
|
|
654
|
+
fn update_advances_reports_true_when_the_update_would_park() {
|
|
655
|
+
// Defense in depth for callers using advances? without the ready gate: a
|
|
656
|
+
// gappy update parks pending — that changes the doc, so it advances (it
|
|
657
|
+
// must never be misread as an already-applied retry and dropped).
|
|
658
|
+
let (_c_update, a_delta) = cross_client_origin_gap();
|
|
659
|
+
let server = Doc::new();
|
|
660
|
+
assert!(
|
|
661
|
+
update_advances_doc(&server, &a_delta).unwrap(),
|
|
662
|
+
"a parked update is not a duplicate"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
467
666
|
// Build a causal gap: `first` inserts "a", `dependent` inserts "b" after it,
|
|
468
667
|
// so `dependent` alone parks as pending on a doc that lacks `first`.
|
|
469
668
|
fn gap_pair() -> (Vec<u8>, Vec<u8>) {
|
|
@@ -627,6 +826,58 @@ mod tests {
|
|
|
627
826
|
assert!(!has_pending(&peer), "the diff carried no pending");
|
|
628
827
|
}
|
|
629
828
|
|
|
829
|
+
#[test]
|
|
830
|
+
fn integrated_update_never_serves_pending_under_concurrent_gappy_applies() {
|
|
831
|
+
// Invariant under contention: while a writer parks and heals a gappy
|
|
832
|
+
// update in a loop, every integrated_update encode must be pending-free
|
|
833
|
+
// for a fresh peer.
|
|
834
|
+
//
|
|
835
|
+
// Scope: this can't hit the original check-vs-encode race (its window
|
|
836
|
+
// is nanoseconds; never reproduced even at 20k iterations) — that fix
|
|
837
|
+
// is guaranteed by using a single transaction. What this catches is
|
|
838
|
+
// coarser: encoding outside the lock, or a fast path skipping the
|
|
839
|
+
// pending check.
|
|
840
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
841
|
+
use std::sync::Arc as StdArc;
|
|
842
|
+
|
|
843
|
+
let (first, dependent) = gap_pair();
|
|
844
|
+
let doc = StdArc::new(Doc::new());
|
|
845
|
+
let stop = StdArc::new(AtomicBool::new(false));
|
|
846
|
+
|
|
847
|
+
let writer = {
|
|
848
|
+
let doc = StdArc::clone(&doc);
|
|
849
|
+
let stop = StdArc::clone(&stop);
|
|
850
|
+
let dependent = dependent.clone();
|
|
851
|
+
let first = first.clone();
|
|
852
|
+
std::thread::spawn(move || {
|
|
853
|
+
while !stop.load(Ordering::Relaxed) {
|
|
854
|
+
// Park a pending struct, then heal it, over and over — the
|
|
855
|
+
// encode below keeps racing both transitions.
|
|
856
|
+
doc.transact_mut()
|
|
857
|
+
.apply_update(yrs::Update::decode_v1(&dependent).unwrap())
|
|
858
|
+
.unwrap();
|
|
859
|
+
doc.transact_mut()
|
|
860
|
+
.apply_update(yrs::Update::decode_v1(&first).unwrap())
|
|
861
|
+
.unwrap();
|
|
862
|
+
}
|
|
863
|
+
})
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
for _ in 0..500 {
|
|
867
|
+
let encoded = integrated_update(&doc, &yrs::StateVector::default()).unwrap();
|
|
868
|
+
let peer = Doc::new();
|
|
869
|
+
peer.transact_mut()
|
|
870
|
+
.apply_update(yrs::Update::decode_v1(&encoded).unwrap())
|
|
871
|
+
.unwrap();
|
|
872
|
+
assert!(
|
|
873
|
+
!has_pending(&peer),
|
|
874
|
+
"an integrated_update encode leaked pending to a peer"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
stop.store(true, Ordering::Relaxed);
|
|
878
|
+
writer.join().unwrap();
|
|
879
|
+
}
|
|
880
|
+
|
|
630
881
|
#[test]
|
|
631
882
|
fn integrated_update_strips_a_pending_delete_set() {
|
|
632
883
|
// A deletion whose target struct is absent parks as a pending *delete
|
data/ext/yrby/src/read.rs
CHANGED
|
@@ -62,6 +62,17 @@ fn lexical_type<T: ReadTxn>(txn: &T, t: &XmlTextRef) -> String {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/// The `__type` of an embedded Lexical `Y.Map`. Two kinds appear inside a
|
|
66
|
+
/// block: text-node metadata (`"text"`) and node maps like the LineBreakNode
|
|
67
|
+
/// (`"linebreak"`). Structure confirmed from live-editor bytes (see the
|
|
68
|
+
/// captured-fixture test).
|
|
69
|
+
fn lexical_map_type<T: ReadTxn>(txn: &T, m: &MapRef) -> String {
|
|
70
|
+
match m.get(txn, "__type") {
|
|
71
|
+
Some(Out::Any(Any::String(s))) => s.to_string(),
|
|
72
|
+
_ => String::new(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
/// Gather the text of an inline Lexical element (its text runs and any nested
|
|
66
77
|
/// inline elements) without introducing block breaks.
|
|
67
78
|
fn inline_lexical_text<T: ReadTxn>(txn: &T, t: &XmlTextRef, buf: &mut String) {
|
|
@@ -69,7 +80,12 @@ fn inline_lexical_text<T: ReadTxn>(txn: &T, t: &XmlTextRef, buf: &mut String) {
|
|
|
69
80
|
match d.insert {
|
|
70
81
|
Out::Any(Any::String(s)) => buf.push_str(&s),
|
|
71
82
|
Out::YXmlText(child) => inline_lexical_text(txn, &child, buf),
|
|
72
|
-
|
|
83
|
+
Out::YMap(m) => match lexical_map_type(txn, &m).as_str() {
|
|
84
|
+
"linebreak" => buf.push('\n'),
|
|
85
|
+
"tab" => buf.push('\t'),
|
|
86
|
+
_ => {} // per-text-node metadata: no text of its own
|
|
87
|
+
},
|
|
88
|
+
_ => {} // decorator embeds: no text
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
}
|
|
@@ -81,17 +97,30 @@ fn walk_lexical_block<T: ReadTxn>(txn: &T, t: &XmlTextRef, out: &mut Vec<String>
|
|
|
81
97
|
for d in t.diff(txn, YChange::identity) {
|
|
82
98
|
match d.insert {
|
|
83
99
|
Out::Any(Any::String(s)) => line.push_str(&s),
|
|
100
|
+
// Node maps: linebreak/tab carry no text, so emit the character
|
|
101
|
+
// they represent ("foo⏎bar" must not become "foobar"). Metadata
|
|
102
|
+
// maps ("text") stay silent.
|
|
103
|
+
Out::YMap(m) => match lexical_map_type(txn, &m).as_str() {
|
|
104
|
+
"linebreak" => line.push('\n'),
|
|
105
|
+
"tab" => line.push('\t'),
|
|
106
|
+
_ => {}
|
|
107
|
+
},
|
|
84
108
|
Out::YXmlText(child) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
let ty = lexical_type(txn, &child);
|
|
110
|
+
match ty.as_str() {
|
|
111
|
+
// Defensive only: real Lexical stores these as Y.Map embeds.
|
|
112
|
+
"linebreak" => line.push('\n'),
|
|
113
|
+
"tab" => line.push('\t'),
|
|
114
|
+
_ if is_inline_lexical_type(&ty) => inline_lexical_text(txn, &child, &mut line),
|
|
115
|
+
_ => {
|
|
116
|
+
if !line.is_empty() {
|
|
117
|
+
out.push(std::mem::take(&mut line));
|
|
118
|
+
}
|
|
119
|
+
walk_lexical_block(txn, &child, out);
|
|
90
120
|
}
|
|
91
|
-
walk_lexical_block(txn, &child, out);
|
|
92
121
|
}
|
|
93
122
|
}
|
|
94
|
-
_ => {} //
|
|
123
|
+
_ => {} // decorator embeds we don't read for text
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
if !line.is_empty() {
|
|
@@ -256,6 +285,53 @@ mod tests {
|
|
|
256
285
|
assert_eq!(map_json(&txn, &map), "{}");
|
|
257
286
|
}
|
|
258
287
|
|
|
288
|
+
#[test]
|
|
289
|
+
fn lexical_soft_line_break_and_tab_emit_their_characters() {
|
|
290
|
+
// A paragraph "foo⏎bar" (shift-enter): Lexical stores the LineBreakNode
|
|
291
|
+
// as an embedded Y.Map with __type=linebreak (the same shape as the
|
|
292
|
+
// per-text-node metadata maps, which must stay silent). It must come
|
|
293
|
+
// through as '\n', not vanish and glue the words. Same for tab.
|
|
294
|
+
use yrs::{Text, XmlTextPrelim};
|
|
295
|
+
let doc = Doc::new();
|
|
296
|
+
let frag = doc.get_or_insert_xml_fragment("lex");
|
|
297
|
+
{
|
|
298
|
+
let mut txn = doc.transact_mut();
|
|
299
|
+
let block = frag.push_back(&mut txn, XmlTextPrelim::new(""));
|
|
300
|
+
let meta: MapPrelim = [("__type", yrs::In::from("text"))].into_iter().collect();
|
|
301
|
+
block.insert_embed(&mut txn, 0, meta); // metadata map: no text
|
|
302
|
+
block.push(&mut txn, "foo");
|
|
303
|
+
let br: MapPrelim = [("__type", yrs::In::from("linebreak"))]
|
|
304
|
+
.into_iter()
|
|
305
|
+
.collect();
|
|
306
|
+
block.insert_embed(&mut txn, 4, br);
|
|
307
|
+
block.push(&mut txn, "bar");
|
|
308
|
+
let tab: MapPrelim = [("__type", yrs::In::from("tab"))].into_iter().collect();
|
|
309
|
+
block.insert_embed(&mut txn, 8, tab);
|
|
310
|
+
block.push(&mut txn, "baz");
|
|
311
|
+
}
|
|
312
|
+
let txn = doc.transact();
|
|
313
|
+
assert_eq!(xml_blocks_text(&txn, &frag), "foo\nbar\tbaz");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[test]
|
|
317
|
+
fn lexical_real_captured_linebreak_extracts_as_newline() {
|
|
318
|
+
// Ground truth: bytes captured from a LIVE Lexxy editor (agent-browser
|
|
319
|
+
// typing "foo", pressing Shift+Enter, typing "barbaz"), served by the
|
|
320
|
+
// yrby test server's durable store. The hand-built test above models
|
|
321
|
+
// this structure; this one IS the structure. Regenerate by driving
|
|
322
|
+
// lexxy-realtime's test server and saving GET /content/:room.
|
|
323
|
+
use yrs::updates::decoder::Decode;
|
|
324
|
+
use yrs::Update;
|
|
325
|
+
let bytes = include_bytes!("fixtures/lexical_linebreak.bin");
|
|
326
|
+
let doc = Doc::new();
|
|
327
|
+
doc.transact_mut()
|
|
328
|
+
.apply_update(Update::decode_v1(bytes).unwrap())
|
|
329
|
+
.unwrap();
|
|
330
|
+
let txn = doc.transact();
|
|
331
|
+
let frag = txn.get_xml_fragment("root").unwrap();
|
|
332
|
+
assert_eq!(xml_blocks_text(&txn, &frag), "foo\nbarbaz");
|
|
333
|
+
}
|
|
334
|
+
|
|
259
335
|
#[test]
|
|
260
336
|
fn lexical_complex_doc_extracts_all_nested_text() {
|
|
261
337
|
// A real Lexxy/Lexical doc with every block type: headings, formatted
|
data/lib/y/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yrby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JP Camara
|
|
@@ -77,6 +77,7 @@ extensions:
|
|
|
77
77
|
extra_rdoc_files: []
|
|
78
78
|
files:
|
|
79
79
|
- CHANGELOG.md
|
|
80
|
+
- Cargo.lock
|
|
80
81
|
- Cargo.toml
|
|
81
82
|
- LICENSE
|
|
82
83
|
- README.md
|
|
@@ -86,10 +87,7 @@ files:
|
|
|
86
87
|
- ext/yrby/src/protocol.rs
|
|
87
88
|
- ext/yrby/src/read.rs
|
|
88
89
|
- lib/y.rb
|
|
89
|
-
- lib/y/decoder.rb
|
|
90
|
-
- lib/y/decoder/version.rb
|
|
91
90
|
- lib/y/version.rb
|
|
92
|
-
- lib/yrby-decoder.rb
|
|
93
91
|
- lib/yrby.rb
|
|
94
92
|
homepage: https://github.com/jpcamara/yrby
|
|
95
93
|
licenses:
|
data/lib/y/decoder/version.rb
DELETED
data/lib/y/decoder.rb
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "y"
|
|
4
|
-
require "y/decoder/version"
|
|
5
|
-
|
|
6
|
-
module Y
|
|
7
|
-
# Plain-text reconstruction of a stored Yjs document, in pure Ruby — for search
|
|
8
|
-
# indexing and previews. The core `yrby` gem moves and stores opaque CRDT
|
|
9
|
-
# updates without reading them; this reads the text out of the shared type the
|
|
10
|
-
# editor uses (Lexical's `Y.XmlText`, plain `Y.Text`, or ProseMirror's
|
|
11
|
-
# `Y.XmlFragment`), in-process, on the native extension core already ships — no
|
|
12
|
-
# Node, no subprocess, no binary.
|
|
13
|
-
#
|
|
14
|
-
# state = doc.encode_state_as_update # opaque CRDT bytes from the store
|
|
15
|
-
# Y::Decoder.text(state) # => "hello world"
|
|
16
|
-
# Y::Decoder.preview(state, 280) # => "hello world…"
|
|
17
|
-
#
|
|
18
|
-
# Full-fidelity reconstruction (the exact Lexical EditorState / HTML, which
|
|
19
|
-
# needs @lexical/yjs) is a separate, opt-in concern — see the `yrby-decode`
|
|
20
|
-
# package's Bun binary. This gem stays pure Ruby on purpose.
|
|
21
|
-
module Decoder
|
|
22
|
-
class Error < Y::Error; end
|
|
23
|
-
|
|
24
|
-
module_function
|
|
25
|
-
|
|
26
|
-
# Plain text of the document. `field` pins the root key (Lexical: the editor
|
|
27
|
-
# id; ProseMirror: "default"); omit it to use the document's sole root.
|
|
28
|
-
def text(state, field: nil)
|
|
29
|
-
field ||= Y::Doc.new.tap { |d| d.apply_update(state) }.root_names.first
|
|
30
|
-
return "" unless field
|
|
31
|
-
|
|
32
|
-
# A plain `Y.Text` root (a simple shared-text editor) reads straight out.
|
|
33
|
-
# (A yrs root's type is fixed by its first typed access, so each reader
|
|
34
|
-
# gets a fresh doc to try a different shared type against the same state.)
|
|
35
|
-
direct = load(state).read_text(field)
|
|
36
|
-
return normalize(direct) if direct && !direct.strip.empty?
|
|
37
|
-
|
|
38
|
-
# Lexical (each block a sibling `Y.XmlText`) and ProseMirror (blocks are
|
|
39
|
-
# `Y.XmlElement`s) both come back from read_xml as block-per-line markup;
|
|
40
|
-
# strip any element tags to plain text.
|
|
41
|
-
markup = load(state).read_xml(field)
|
|
42
|
-
markup ? normalize(strip_tags(markup)) : ""
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# A compact, single-line preview for list UIs.
|
|
46
|
-
def preview(state, limit: 280, field: nil)
|
|
47
|
-
body = text(state, field: field).gsub(/\s+/, " ").strip
|
|
48
|
-
body.length > limit ? "#{body[0, limit].rstrip}…" : body
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def load(state)
|
|
52
|
-
Y::Doc.new.tap { |doc| doc.apply_update(state) }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def strip_tags(markup)
|
|
56
|
-
markup.gsub(/<[^>]*>/, " ")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def normalize(text)
|
|
60
|
-
text.gsub(/[ \t]+/, " ") # collapse runs of spaces/tabs
|
|
61
|
-
.gsub(/ *\n */, "\n") # trim spaces left around block separators
|
|
62
|
-
.gsub(/\n{3,}/, "\n\n") # cap blank-line runs
|
|
63
|
-
.strip
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
data/lib/yrby-decoder.rb
DELETED