yrby 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ //! Live `Y::Array` handles over a yrs `Array`: read and write the elements of a
2
+ //! shared list from Ruby.
3
+ //!
4
+ //! Same contract as `Y::Map`. Every operation opens its own transaction inside
5
+ //! `nogvl`, nothing caches a branch pointer, and Ruby values are only touched
6
+ //! with the GVL held.
7
+ //!
8
+ //! The method that earns this type its place is `get_map`, which hands back a
9
+ //! live handle to a map stored at an index. A list of records is the shape most
10
+ //! collaborative state actually has (a plan, a board, a set of rows), and
11
+ //! without a handle you could read one but never edit it in place. The handle
12
+ //! keeps the record's identity, so it stays on that record when others are
13
+ //! inserted or removed around it.
14
+
15
+ use magnus::{prelude::*, typed_data::Obj, Error, Ruby, Value};
16
+ use yrs::branch::BranchID;
17
+ use yrs::{Any, Array, ArrayRef, Doc, Transact};
18
+
19
+ use crate::map::RbMap;
20
+ use crate::nogvl;
21
+ use crate::read::out_to_any;
22
+ use crate::shared::{
23
+ any_to_ruby, element_index, guarded, identity, insert_index, resolve, ruby_to_invalue, to_in,
24
+ Child, ORIGIN,
25
+ };
26
+ use crate::text::RbText;
27
+
28
+ #[magnus::wrap(class = "Y::Array", free_immediately, size)]
29
+ pub struct RbArray {
30
+ doc: Doc,
31
+ id: BranchID,
32
+ }
33
+
34
+ impl RbArray {
35
+ pub fn new(doc: Doc, id: BranchID) -> Self {
36
+ RbArray { doc, id }
37
+ }
38
+
39
+ /// The shared value at `index`, resolved now: what `get_map`, `get_array`
40
+ /// and `get_text` narrow to a handle.
41
+ fn child(&self, index: i64) -> Result<Child, Error> {
42
+ let (doc, id) = (&self.doc, &self.id);
43
+ nogvl(move || {
44
+ let txn = doc.transact();
45
+ Child::of(
46
+ resolve::<ArrayRef, _>(&txn, id)
47
+ .and_then(|a| a.get(&txn, element_index(index, a.len(&txn))?)),
48
+ )
49
+ })
50
+ }
51
+
52
+ // --- reads ---
53
+
54
+ /// `array[i]`: a snapshot value. Nested maps and arrays come back as deep
55
+ /// `Hash`/`Array`; use `get_map` for a live handle instead.
56
+ fn get(&self, index: i64) -> Result<Value, Error> {
57
+ let (doc, id) = (&self.doc, &self.id);
58
+ let got: Option<Any> = nogvl(move || {
59
+ let txn = doc.transact();
60
+ let a = resolve::<ArrayRef, _>(&txn, id)?;
61
+ let i = element_index(index, a.len(&txn))?;
62
+ a.get(&txn, i).map(|v| out_to_any(&txn, &v))
63
+ })?;
64
+ let ruby = Ruby::get().unwrap();
65
+ Ok(match got {
66
+ Some(a) => any_to_ruby(&ruby, &a),
67
+ None => ruby.qnil().as_value(),
68
+ })
69
+ }
70
+
71
+ /// A live `Y::Map` for the element at `index`, or `nil` if that element is
72
+ /// absent or is not a map. Mutating it mutates the document.
73
+ fn get_map(&self, index: i64) -> Result<Option<RbMap>, Error> {
74
+ Ok(match self.child(index)? {
75
+ Child::Map(id) => Some(RbMap::new(self.doc.clone(), id)),
76
+ _ => None,
77
+ })
78
+ }
79
+
80
+ /// A live `Y::Text` for text stored at `index`. An agent appending to one
81
+ /// field of one record goes through here.
82
+ fn get_text(&self, index: i64) -> Result<Option<RbText>, Error> {
83
+ Ok(match self.child(index)? {
84
+ Child::Text(id) => Some(RbText::new(self.doc.clone(), id)),
85
+ _ => None,
86
+ })
87
+ }
88
+
89
+ /// A live `Y::Array` for a nested array at `index`.
90
+ fn get_array(&self, index: i64) -> Result<Option<Self>, Error> {
91
+ Ok(match self.child(index)? {
92
+ Child::Array(id) => Some(RbArray::new(self.doc.clone(), id)),
93
+ _ => None,
94
+ })
95
+ }
96
+
97
+ fn size(&self) -> Result<usize, Error> {
98
+ let (doc, id) = (&self.doc, &self.id);
99
+ nogvl(move || {
100
+ let txn = doc.transact();
101
+ resolve::<ArrayRef, _>(&txn, id)
102
+ .map(|a| a.len(&txn) as usize)
103
+ .unwrap_or(0)
104
+ })
105
+ }
106
+
107
+ fn is_empty(&self) -> Result<bool, Error> {
108
+ Ok(self.size()? == 0)
109
+ }
110
+
111
+ fn snapshot(&self) -> Result<Vec<Any>, Error> {
112
+ let (doc, id) = (&self.doc, &self.id);
113
+ nogvl(move || {
114
+ let txn = doc.transact();
115
+ match resolve::<ArrayRef, _>(&txn, id) {
116
+ Some(a) => a.iter(&txn).map(|v| out_to_any(&txn, &v)).collect(),
117
+ None => Vec::new(),
118
+ }
119
+ })
120
+ }
121
+
122
+ fn to_a(&self) -> Result<Value, Error> {
123
+ let ruby = Ruby::get().unwrap();
124
+ Ok(ruby
125
+ .ary_from_iter(self.snapshot()?.iter().map(|v| any_to_ruby(&ruby, v)))
126
+ .as_value())
127
+ }
128
+
129
+ /// `each { |value| }`: yields a snapshot of each element (read under
130
+ /// `nogvl`, yielded with the GVL held so the block can call into Ruby),
131
+ /// and returns the array. Without a block, an Enumerator.
132
+ fn each(ruby: &Ruby, rb_self: Obj<Self>) -> Result<Value, Error> {
133
+ if !ruby.block_given() {
134
+ return Ok(rb_self.enumeratorize("each", ()).as_value());
135
+ }
136
+ let block = ruby.block_proc()?;
137
+ for v in &rb_self.snapshot()? {
138
+ let _: Value = block.call((any_to_ruby(ruby, v),))?;
139
+ }
140
+ Ok(rb_self.as_value())
141
+ }
142
+
143
+ // --- writes ---
144
+
145
+ /// Append a value and return the array, as `Array#push` does. A Ruby
146
+ /// `Hash` becomes a real nested `Y.Map`, so the element can be handed back
147
+ /// later as a live handle by `get_map`.
148
+ fn push(rb_self: Obj<Self>, value: Value) -> Result<Obj<Self>, Error> {
149
+ rb_self.insert_at(-1, value)?;
150
+ Ok(rb_self)
151
+ }
152
+
153
+ /// Insert at `index` and return the array, as `Array#insert` does.
154
+ fn insert(rb_self: Obj<Self>, index: i64, value: Value) -> Result<Obj<Self>, Error> {
155
+ rb_self.insert_at(index, value)?;
156
+ Ok(rb_self)
157
+ }
158
+
159
+ /// Insert at `index`: a negative index counts from the end (-1 appends),
160
+ /// and an index past the end appends.
161
+ fn insert_at(&self, index: i64, value: Value) -> Result<(), Error> {
162
+ let ruby = Ruby::get().unwrap();
163
+ let iv = ruby_to_invalue(&ruby, value)?;
164
+ let (doc, id) = (&self.doc, &self.id);
165
+ guarded(move || {
166
+ let mut txn = doc.transact_mut_with(ORIGIN);
167
+ let a = resolve::<ArrayRef, _>(&txn, id).ok_or("array no longer exists")?;
168
+ let at = insert_index(index, a.len(&txn));
169
+ a.insert(&mut txn, at, to_in(iv));
170
+ Ok(())
171
+ })
172
+ }
173
+
174
+ /// Remove the element at `index`, returning its previous snapshot value.
175
+ fn delete_at(&self, index: i64) -> Result<Value, Error> {
176
+ let (doc, id) = (&self.doc, &self.id);
177
+ let prev: Option<Any> = guarded(move || {
178
+ let mut txn = doc.transact_mut_with(ORIGIN);
179
+ let Some(a) = resolve::<ArrayRef, _>(&txn, id) else {
180
+ return Ok(None);
181
+ };
182
+ let Some(i) = element_index(index, a.len(&txn)) else {
183
+ return Ok(None);
184
+ };
185
+ // Convert before removing: a removed shared ref would dangle.
186
+ let prev = a.get(&txn, i).map(|v| out_to_any(&txn, &v));
187
+ a.remove(&mut txn, i);
188
+ Ok(prev)
189
+ })?;
190
+ let ruby = Ruby::get().unwrap();
191
+ Ok(match prev {
192
+ Some(a) => any_to_ruby(&ruby, &a),
193
+ None => ruby.qnil().as_value(),
194
+ })
195
+ }
196
+
197
+ fn clear(&self) -> Result<(), Error> {
198
+ let (doc, id) = (&self.doc, &self.id);
199
+ guarded(move || {
200
+ let mut txn = doc.transact_mut_with(ORIGIN);
201
+ if let Some(a) = resolve::<ArrayRef, _>(&txn, id) {
202
+ let len = a.len(&txn);
203
+ if len > 0 {
204
+ a.remove_range(&mut txn, 0, len);
205
+ }
206
+ }
207
+ Ok(())
208
+ })
209
+ }
210
+
211
+ /// Replace the element at `index`. Yjs arrays have no in-place update, so
212
+ /// this is a remove plus an insert in one transaction. The element gets a
213
+ /// new identity, which is why a live handle to it must be re-fetched.
214
+ fn set(&self, index: i64, value: Value) -> Result<Value, Error> {
215
+ let ruby = Ruby::get().unwrap();
216
+ let iv = ruby_to_invalue(&ruby, value)?;
217
+ let (doc, id) = (&self.doc, &self.id);
218
+ guarded(move || {
219
+ let mut txn = doc.transact_mut_with(ORIGIN);
220
+ let a = resolve::<ArrayRef, _>(&txn, id).ok_or("array no longer exists")?;
221
+ let i = element_index(index, a.len(&txn)).ok_or("index out of range")?;
222
+ a.remove(&mut txn, i);
223
+ a.insert(&mut txn, i, to_in(iv));
224
+ Ok(())
225
+ })?;
226
+ Ok(value)
227
+ }
228
+ }
229
+
230
+ /// Ensure a root array exists and return a live handle. Called by
231
+ /// `Y::Doc#get_array`.
232
+ pub fn root_array(doc: &Doc, name: String) -> Result<RbArray, Error> {
233
+ let d = doc.clone();
234
+ let id = nogvl(move || identity(&d.get_or_insert_array(name.as_str())))?;
235
+ Ok(RbArray::new(doc.clone(), id))
236
+ }
237
+
238
+ pub fn define(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
239
+ let class = module.define_class("Array", ruby.class_object())?;
240
+ class.define_method("[]", magnus::method!(RbArray::get, 1))?;
241
+ class.define_method("get", magnus::method!(RbArray::get, 1))?;
242
+ class.define_method("[]=", magnus::method!(RbArray::set, 2))?;
243
+ class.define_method("get_map", magnus::method!(RbArray::get_map, 1))?;
244
+ class.define_method("get_array", magnus::method!(RbArray::get_array, 1))?;
245
+ class.define_method("get_text", magnus::method!(RbArray::get_text, 1))?;
246
+ class.define_method("push", magnus::method!(RbArray::push, 1))?;
247
+ class.define_method("<<", magnus::method!(RbArray::push, 1))?;
248
+ class.define_method("insert", magnus::method!(RbArray::insert, 2))?;
249
+ class.define_method("delete_at", magnus::method!(RbArray::delete_at, 1))?;
250
+ class.define_method("clear", magnus::method!(RbArray::clear, 0))?;
251
+ class.define_method("size", magnus::method!(RbArray::size, 0))?;
252
+ class.define_method("length", magnus::method!(RbArray::size, 0))?;
253
+ class.define_method("empty?", magnus::method!(RbArray::is_empty, 0))?;
254
+ class.define_method("to_a", magnus::method!(RbArray::to_a, 0))?;
255
+ class.define_method("each", magnus::method!(RbArray::each, 0))?;
256
+ Ok(())
257
+ }
258
+
259
+ #[cfg(test)]
260
+ mod tests {
261
+ //! Each method on a handle, called as Ruby calls it.
262
+ use super::*;
263
+ use crate::shared::test_support::{doc, hash, int, rb};
264
+ use rb_sys_test_helpers::ruby_test;
265
+ use yrs::{ArrayPrelim, TextPrelim, Transact};
266
+
267
+ fn list_of(values: &[i64]) -> RbArray {
268
+ let list = root_array(&doc(), "list".into()).unwrap();
269
+ for v in values {
270
+ list.insert_at(-1, rb(*v)).unwrap();
271
+ }
272
+ list
273
+ }
274
+
275
+ #[ruby_test]
276
+ fn a_fresh_array_is_empty() {
277
+ let list = list_of(&[]);
278
+ assert_eq!(list.size().unwrap(), 0);
279
+ assert!(list.is_empty().unwrap());
280
+ assert_eq!(list.to_a().unwrap().inspect(), "[]");
281
+ }
282
+
283
+ #[ruby_test]
284
+ fn insert_at_minus_one_appends() {
285
+ let list = list_of(&[1]);
286
+ list.insert_at(-1, rb(2)).unwrap();
287
+ assert_eq!(list.snapshot().unwrap(), vec![int(1), int(2)]);
288
+ }
289
+
290
+ #[ruby_test]
291
+ fn insert_reads_a_negative_index_from_the_end_and_clamps() {
292
+ let list = list_of(&[1, 3]);
293
+ list.insert_at(1, rb(2)).unwrap();
294
+ list.insert_at(-1, rb(4)).unwrap();
295
+ list.insert_at(99, rb(5)).unwrap();
296
+ list.insert_at(-99, rb(0)).unwrap();
297
+ assert_eq!(
298
+ list.snapshot().unwrap(),
299
+ vec![int(0), int(1), int(2), int(3), int(4), int(5)]
300
+ );
301
+ }
302
+
303
+ #[ruby_test]
304
+ fn get_reads_a_negative_index_and_is_nil_outside() {
305
+ let list = list_of(&[1, 2, 3]);
306
+ assert_eq!(list.get(0).unwrap().inspect(), "1");
307
+ assert_eq!(list.get(-1).unwrap().inspect(), "3");
308
+ assert_eq!(list.get(3).unwrap().inspect(), "nil");
309
+ assert_eq!(list.get(-4).unwrap().inspect(), "nil");
310
+ }
311
+
312
+ #[ruby_test]
313
+ fn set_replaces_in_place_and_raises_outside() {
314
+ let list = list_of(&[1, 2]);
315
+ list.set(-1, rb(9)).unwrap();
316
+ assert_eq!(list.snapshot().unwrap(), vec![int(1), int(9)]);
317
+ let err = list.set(2, rb(3)).unwrap_err();
318
+ assert!(err.to_string().contains("index out of range"), "{err}");
319
+ }
320
+
321
+ #[ruby_test]
322
+ fn delete_at_returns_the_removed_value_or_nil() {
323
+ let list = list_of(&[1, 2, 3]);
324
+ assert_eq!(list.delete_at(1).unwrap().inspect(), "2");
325
+ assert_eq!(list.delete_at(5).unwrap().inspect(), "nil");
326
+ assert_eq!(list.snapshot().unwrap(), vec![int(1), int(3)]);
327
+ }
328
+
329
+ #[ruby_test]
330
+ fn clear_removes_every_element() {
331
+ let list = list_of(&[1, 2, 3]);
332
+ list.clear().unwrap();
333
+ assert!(list.is_empty().unwrap());
334
+ }
335
+
336
+ #[ruby_test]
337
+ fn child_is_the_shared_value_at_an_index() {
338
+ let list = list_of(&[1]);
339
+ list.insert_at(-1, hash(&[("name", rb("Ada"))]).as_value())
340
+ .unwrap();
341
+ assert!(matches!(list.child(-1).unwrap(), Child::Map(_)));
342
+ assert!(matches!(list.child(0).unwrap(), Child::Other));
343
+ assert!(matches!(list.child(2).unwrap(), Child::Other));
344
+ }
345
+
346
+ #[ruby_test]
347
+ fn get_map_get_text_and_get_array_narrow_by_type() {
348
+ let list = list_of(&[]);
349
+ list.insert_at(-1, hash(&[("name", rb("Ada"))]).as_value())
350
+ .unwrap();
351
+ {
352
+ let mut txn = list.doc.transact_mut();
353
+ let array = resolve::<ArrayRef, _>(&txn, &list.id).unwrap();
354
+ array.push_back(&mut txn, TextPrelim::new("hi"));
355
+ array.push_back(&mut txn, ArrayPrelim::default());
356
+ }
357
+ assert!(list.get_map(0).unwrap().is_some());
358
+ assert_eq!(list.get(0).unwrap().inspect(), r#"{"name" => "Ada"}"#);
359
+ assert!(list.get_text(0).unwrap().is_none());
360
+ assert!(list.get_text(1).unwrap().is_some());
361
+ assert!(list.get_map(1).unwrap().is_none());
362
+ let nested = list.get_array(-1).unwrap().expect("an array at the end");
363
+ nested.insert_at(-1, rb(7)).unwrap();
364
+ assert_eq!(list.get(2).unwrap().inspect(), "[7]");
365
+ }
366
+
367
+ #[ruby_test]
368
+ fn a_handle_to_a_deleted_array_reads_empty_and_refuses_writes() {
369
+ let outer = list_of(&[]);
370
+ {
371
+ let mut txn = outer.doc.transact_mut();
372
+ let array = resolve::<ArrayRef, _>(&txn, &outer.id).unwrap();
373
+ array.push_back(&mut txn, ArrayPrelim::default());
374
+ }
375
+ let inner = outer.get_array(0).unwrap().unwrap();
376
+ inner.insert_at(-1, rb(1)).unwrap();
377
+ outer.delete_at(0).unwrap();
378
+ assert_eq!(inner.size().unwrap(), 0);
379
+ assert!(inner.snapshot().unwrap().is_empty());
380
+ let err = inner.insert_at(-1, rb(2)).unwrap_err();
381
+ assert!(err.to_string().contains("array no longer exists"), "{err}");
382
+ }
383
+
384
+ #[ruby_test]
385
+ fn root_array_returns_the_same_array_for_a_name() {
386
+ let doc = doc();
387
+ root_array(&doc, "list".into())
388
+ .unwrap()
389
+ .insert_at(-1, rb(1))
390
+ .unwrap();
391
+ assert_eq!(
392
+ root_array(&doc, "list".into()).unwrap().snapshot().unwrap(),
393
+ vec![int(1)]
394
+ );
395
+ }
396
+ }