y-rb 0.1.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.
data/lib/y/array.rb ADDED
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ # An array can be used to store and retrieve elements.
5
+ #
6
+ # The array is the replicated counterpart to a Ruby Array. It supports a
7
+ # subset of the Ruby Array operations, like adding, getting and deleting
8
+ # values by position or ranges.
9
+ #
10
+ # Someone should not instantiate an array directly, but use {Y::Doc#get_array}
11
+ # instead.
12
+ #
13
+ # @example
14
+ # doc = Y::Doc.new
15
+ # array = doc.get_array("my array")
16
+ #
17
+ # array << 1
18
+ # array.push(2)
19
+ # array.concat([3, 4, 5])
20
+ #
21
+ # array.to_a == [1, 2, 3, 4, 5] # true
22
+ class Array
23
+ # @!attribute [r] document
24
+ #
25
+ # @return [Y::Doc] The document this array belongs to
26
+ attr_accessor :document
27
+
28
+ # Create a new array instance
29
+ #
30
+ # @param [Y::Doc] doc
31
+ def initialize(doc = nil)
32
+ @document = doc || Y::Doc.new
33
+
34
+ super()
35
+ end
36
+
37
+ # Retrieves element at position
38
+ #
39
+ # @return [Object]
40
+ def [](index)
41
+ yarray_get(index)
42
+ end
43
+
44
+ # Inserts value at position
45
+ #
46
+ # @param [Integer] index
47
+ # @param [true|false|Float|Integer|String|Array|Hash] value
48
+ # @return [void]
49
+ def []=(index, value)
50
+ yarray_insert(transaction, index, value)
51
+ end
52
+
53
+ # Adds an element to the end of the array
54
+ #
55
+ # @return [void]
56
+ def <<(value)
57
+ yarray_push_back(transaction, value)
58
+ end
59
+
60
+ # Attach listener to array changes
61
+ #
62
+ # @example Listen to changes in array type
63
+ # local = Y::Doc.new
64
+ #
65
+ # arr = local.get_array("my array")
66
+ # arr.attach(->(delta) { pp delta })
67
+ #
68
+ # local.transact do
69
+ # arr << 1
70
+ # end
71
+ #
72
+ # @param [Proc] callback
73
+ # @param [Block] block
74
+ # @return [Integer]
75
+ def attach(callback, &block)
76
+ return yarray_observe(callback) unless callback.nil?
77
+
78
+ yarray_observe(block.to_proc) unless block.nil?
79
+ end
80
+
81
+ # Adds to array all elements from each Array in `other_arrays`.
82
+ #
83
+ # If one of the arguments isn't an Array, it is silently ignored.
84
+ #
85
+ # @example Add multiple values to array
86
+ # doc = Y::Doc.new
87
+ # arr = doc.get_array("my array")
88
+ # arr.concat([1, 2, 3])
89
+ #
90
+ # arr.to_a == [1, 2, 3] # true
91
+ #
92
+ # @param [Array<Array<Object>>] other_arrays
93
+ # @return [void]
94
+ def concat(*other_arrays)
95
+ combined = other_arrays.reduce([]) do |values, arr|
96
+ values.concat(arr) if arr.is_a?(::Array)
97
+ end
98
+
99
+ yarray_insert_range(transaction, size, combined)
100
+ end
101
+
102
+ # Detach listener
103
+ #
104
+ # @param [Integer] subscription_id
105
+ # @return [void]
106
+ def detach(subscription_id)
107
+ yarray_unobserve(subscription_id)
108
+ end
109
+
110
+ # @return [void]
111
+ def each(&block)
112
+ yarray_each(block)
113
+ end
114
+
115
+ # Check if the array is empty
116
+ #
117
+ # @return [true|false]
118
+ def empty?
119
+ size.zero?
120
+ end
121
+
122
+ # Returns first element in array if there is at least one
123
+ #
124
+ # @return [Object|nil]
125
+ def first
126
+ yarray_get(0)
127
+ end
128
+
129
+ # Returns last element in array if there is at least one element
130
+ #
131
+ # @return [Object|nil]
132
+ def last
133
+ len = yarray_length
134
+ return yarray_get(yarray_length - 1) if len.positive?
135
+
136
+ nil
137
+ end
138
+
139
+ # rubocop:disable Naming/MethodParameterName
140
+
141
+ # Removes last (n) element(s) from array
142
+ #
143
+ # @param [Integer|nil] n Number of elements to remove
144
+ # @return [void]
145
+ def pop(n = nil)
146
+ len = size
147
+ yarray_remove(transaction, len - 1) if n.nil?
148
+ yarray_remove_range(transaction, len - n, n) unless n.nil?
149
+ end
150
+
151
+ # rubocop:enable Naming/MethodParameterName
152
+
153
+ alias push <<
154
+
155
+ # rubocop:disable Naming/MethodParameterName
156
+
157
+ # Removes first (n) element(s) from array
158
+ #
159
+ # @param [Integer|nil] n Number of elements to remove
160
+ # @return [void]
161
+ def shift(n = nil)
162
+ yarray_remove(transaction, 0) if n.nil?
163
+ yarray_remove_range(transaction, 0, n) unless nil?
164
+ end
165
+
166
+ # rubocop:enable Naming/MethodParameterName
167
+
168
+ # Size of array
169
+ #
170
+ # @return [Integer]
171
+ def size
172
+ yarray_length
173
+ end
174
+
175
+ alias length size
176
+
177
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
178
+
179
+ # Removes one or more elements from array
180
+ #
181
+ # **Attention:** In comparison to Array#slice, {Array#slice!} will not
182
+ # return the values that got removed. Even this being technically
183
+ # possible, it requires us to read the elements before removing them, which
184
+ # is not desirable in most situations.
185
+ #
186
+ # @example Removes a single element
187
+ # doc = Y::Doc.new
188
+ #
189
+ # arr = doc.get_text("my array")
190
+ # arr << 1
191
+ # arr << 2
192
+ # arr << 3
193
+ #
194
+ # arr.slice!(1)
195
+ #
196
+ # arr.to_a == [1, 3] # true
197
+ #
198
+ # @overload slice!(n)
199
+ # Removes nth element from array
200
+ #
201
+ # @overload slice!(start, length)
202
+ # Removes a range of elements
203
+ #
204
+ # @overload slice!(range)
205
+ # Removes a range of elements
206
+ #
207
+ # @return [void]
208
+ def slice!(*args)
209
+ if args.size.zero?
210
+ raise ArgumentError,
211
+ "Provide one of `index`, `range`, `start, length` as arguments"
212
+ end
213
+
214
+ if args.size == 1
215
+ arg = args.first
216
+
217
+ if arg.is_a?(Range)
218
+ if arg.exclude_end?
219
+ yarray_remove_range(transaction, arg.first,
220
+ arg.last - arg.first)
221
+ end
222
+ unless arg.exclude_end?
223
+ yarray_remove_range(transaction, arg.first,
224
+ arg.last + 1 - arg.first)
225
+ end
226
+ return nil
227
+ end
228
+
229
+ if arg.is_a?(Numeric)
230
+ yarray_remove(transaction, arg.to_int)
231
+ return nil
232
+ end
233
+ end
234
+
235
+ if args.size == 2
236
+ first, second = args
237
+
238
+ if first.is_a?(Numeric) && second.is_a?(Numeric)
239
+ yarray_remove_range(transaction, first, second)
240
+ return nil
241
+ end
242
+ end
243
+
244
+ raise ArgumentError, "Please check your arguments, can't slice."
245
+ end
246
+
247
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
248
+
249
+ # Convert this array to a Ruby Array
250
+ #
251
+ # @return [Array<Object>]
252
+ def to_a
253
+ yarray_to_a
254
+ end
255
+
256
+ # Adds an element to the beginning of the array
257
+ #
258
+ # @return [void]
259
+ def unshift(value)
260
+ yarray_push_front(transaction, value)
261
+ end
262
+
263
+ alias prepend unshift
264
+
265
+ private
266
+
267
+ # @!method yarray_each(proc)
268
+ # Iterates over all elements in Array by calling the provided proc
269
+ # with the value as argument.
270
+ #
271
+ # @param [Proc<Object>] proc A proc that is called for every element
272
+
273
+ # @!method yarray_get(index)
274
+ # Retrieves content as specified index
275
+ #
276
+ # @param [Integer] index
277
+ # @return [Object]
278
+
279
+ # @!method yarray_insert(transaction, index, content)
280
+ # Inserts content at specified index
281
+ #
282
+ # @param [Y::Transaction] transaction
283
+ # @param [Integer] index
284
+ # @param [Boolean, Float, Integer, Array, Hash, Text] content
285
+ # @return [void]
286
+
287
+ # @!method yarray_insert_range(transaction, index, arr)
288
+ # Inserts all elements of a given array at specified index
289
+ #
290
+ # @param [Y::Transaction] transaction
291
+ # @param [Integer] index
292
+ # @param [Array<Boolean, Float, Integer, Array, Hash, Text>] arr
293
+ # @return [void]
294
+
295
+ # @!method yarray_length
296
+ # Returns length of array
297
+ #
298
+ # @return [Integer] Length of array
299
+
300
+ # @!method yarray_push_back(transaction, value)
301
+ # Adds an element to the end of the array
302
+ #
303
+ # @param [Y::Transaction] transaction
304
+ # @param [Object] value
305
+ # @return [void]
306
+
307
+ # @!method yarray_push_front(transaction, value)
308
+ # Adds an element to the front of the array
309
+ #
310
+ # @param [Y::Transaction] transaction
311
+ # @param [Object] value
312
+ # @return [void]
313
+
314
+ # @!method yarray_observe(callback)
315
+ #
316
+ # @param [Proc] callback
317
+ # @return [Integer]
318
+
319
+ # @!method yarray_remove(transaction, index)
320
+ # Removes a single element from array at index
321
+ #
322
+ # @param [Y::Transaction] transaction
323
+ # @param [Integer] index
324
+ # @return [void]
325
+
326
+ # @!method yarray_remove_range(transaction, index, length)
327
+ # Removes a range of elements from array
328
+ #
329
+ # @param [Y::Transaction] transaction
330
+ # @param [Integer] index
331
+ # @param [Integer] length
332
+ # @return [void]
333
+
334
+ # @!method yarray_to_a
335
+ # Transforms the array into a Ruby array
336
+ #
337
+ # @return [Array]
338
+
339
+ # @!method yarray_unobserve(subscription_id)
340
+ #
341
+ # @param [Integer] subscription_id
342
+ # @return [void]
343
+
344
+ # A reference to the current active transaction of the document this map
345
+ # belongs to.
346
+ #
347
+ # @return [Y::Transaction] A transaction object
348
+ def transaction
349
+ document.current_transaction
350
+ end
351
+ end
352
+ end
data/lib/y/doc.rb ADDED
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "transaction"
4
+
5
+ module Y
6
+ # @example Create a local and remote doc and syncs the diff
7
+ # local = Y::Doc.new
8
+ # local_map = local.get_map("my map")
9
+ # local_map[:hello] = "world"
10
+ #
11
+ # remote = Y::Doc.new
12
+ #
13
+ # diff = local.diff(remote.state)
14
+ # remote.sync(diff)
15
+ #
16
+ # remote_map = remote.get_map("my_map")
17
+ # pp remote_map.to_h #=> {hello: "world"}
18
+ class Doc
19
+ # Commit current transaction
20
+ #
21
+ # This is a convenience method that invokes {Y::Transaction#commit} on the
22
+ # current transaction used by this document.
23
+ #
24
+ # @return [void]
25
+ def commit
26
+ current_transaction.commit
27
+ end
28
+
29
+ # The currently active transaction for this document
30
+ # @return [Y::Transaction]
31
+ def current_transaction
32
+ @current_transaction ||= begin
33
+ transaction = ydoc_transact
34
+ transaction.document = self
35
+ transaction
36
+ end
37
+ end
38
+
39
+ # Create a diff between this document and another document. The diff is
40
+ # created based on a state vector provided by the other document. It only
41
+ # returns the missing blocks, as binary encoded sequence.
42
+ #
43
+ # @param [::Array<Int>] state The state to create the diff against
44
+ # @return [::Array<Int>] Binary encoded diff
45
+ def diff(state)
46
+ ydoc_encode_diff_v1(state)
47
+ end
48
+
49
+ # Creates a full diff for the current document. It is similar to {#diff},
50
+ # but does not take a state. Instead it creates an empty state and passes it
51
+ # to the encode_diff function.
52
+ #
53
+ # @return [::Array<Int>] Binary encoded diff
54
+ def full_diff
55
+ empty_state = Y::Doc.new.state
56
+ ydoc_encode_diff_v1(empty_state)
57
+ end
58
+
59
+ # Gets or creates a new array by name
60
+ #
61
+ # If the optional values array is present, fills the array up with elements
62
+ # from the provided array. If the array already exists and isn't
63
+ # empty, elements are pushed to the end of the array.
64
+ #
65
+ # @param [String] name The name of the structure
66
+ # @param [::Array] values Optional initial values
67
+ # @return [Y::Array]
68
+ def get_array(name, values = nil)
69
+ array = current_transaction.get_array(name)
70
+ array.document = self
71
+ array.concat(values) unless values.nil?
72
+ array
73
+ end
74
+
75
+ # Gets or creates a new map by name
76
+ #
77
+ # If the optional input hash is present, fills the map up with key-value
78
+ # pairs from the provided input hash. If the map already exists and isn't
79
+ # empty, any existing keys are overridden and new keys are added.
80
+ #
81
+ # @param [String] name The name of the structure
82
+ # @param [Hash] input Optional initial map key-value pairs
83
+ # @return [Y::Map]
84
+ def get_map(name, input = nil)
85
+ map = current_transaction.get_map(name)
86
+ map.document = self
87
+ input&.each { |key, value| map[key] = value }
88
+ map
89
+ end
90
+
91
+ # Gets or creates a new text by name
92
+ #
93
+ # If the optional input string is provided, fills a new text with the string
94
+ # at creation time. If the text isn't new and not empty, appends the input
95
+ # to the end of the text.
96
+ #
97
+ # @param [String] name The name of the structure
98
+ # @param [String] input Optional initial text value
99
+ # @return [Y::Text]
100
+ def get_text(name, input = nil)
101
+ text = current_transaction.get_text(name)
102
+ text.document = self
103
+ text.push(input) unless input.nil?
104
+ text
105
+ end
106
+
107
+ # Gets or creates a new XMLElement by name
108
+ #
109
+ # @param [String] name The name of the structure
110
+ # @return [Y::XMLElement]
111
+ def get_xml_element(name)
112
+ xml_element = current_transaction.get_xml_element(name)
113
+ xml_element.document = self
114
+ xml_element
115
+ end
116
+
117
+ # Gets or creates a new XMLText by name
118
+ #
119
+ # @param [String] name The name of the structure
120
+ # @param [String] input Optional initial text value
121
+ # @return [Y::XMLText]
122
+ def get_xml_text(name, input = nil)
123
+ xml_text = current_transaction.get_xml_text(name)
124
+ xml_text.document = self
125
+ xml_text.push(input) unless input.nil?
126
+ xml_text
127
+ end
128
+
129
+ # Creates a state vector of this document. This can be used to compare the
130
+ # state of two documents with each other and to later on sync them.
131
+ #
132
+ # @return [::Array<Int>] Binary encoded state vector
133
+ def state
134
+ current_transaction.state
135
+ end
136
+
137
+ # Synchronizes this document with the diff from another document
138
+ #
139
+ # @param [::Array<Int>] diff Binary encoded update
140
+ # @return [void]
141
+ def sync(diff)
142
+ current_transaction.apply(diff)
143
+ end
144
+
145
+ # Restores a specific document from an update that contains full state
146
+ #
147
+ # This is doing the same as {#sync}, but it exists to be explicit about
148
+ # the intent. This is the companion to {#full_diff}.
149
+ #
150
+ # @param [::Array<Int>] full_diff Binary encoded update
151
+ # @return [void]
152
+ def restore(full_diff)
153
+ current_transaction.apply(full_diff)
154
+ end
155
+
156
+ # rubocop:disable Metrics/MethodLength
157
+
158
+ # Creates a new transaction and provides it to the given block
159
+ #
160
+ # @example Insert into text
161
+ # doc = Y::Doc.new
162
+ # text = doc.get_text("my text")
163
+ #
164
+ # doc.transact do
165
+ # text << "Hello, World!"
166
+ # end
167
+ #
168
+ # @yield [transaction]
169
+ # @yieldparam [Y::Transaction] transaction
170
+ # @yieldreturn [void]
171
+ # @return [Y::Transaction]
172
+ def transact
173
+ current_transaction.commit
174
+
175
+ if block_given?
176
+ # create new transaction just for the lifetime of this block
177
+ tmp_transaction = ydoc_transact
178
+ tmp_transaction.document = self
179
+
180
+ # override transaction for the lifetime of the block
181
+ @current_transaction = tmp_transaction
182
+
183
+ yield tmp_transaction
184
+
185
+ tmp_transaction.commit
186
+ end
187
+
188
+ # create new transaction
189
+ @current_transaction = ydoc_transact
190
+ @current_transaction.document = self
191
+
192
+ current_transaction
193
+ end
194
+
195
+ # rubocop:enable Metrics/MethodLength
196
+
197
+ # @!method ydoc_encode_diff_v1
198
+ # Encodes the diff of current document state vs provided state
199
+ #
200
+ # @example Create transaction on doc
201
+ # doc = Y::Doc.new
202
+ # tx = doc.ydoc_encode_diff_v1(other_state)
203
+ #
204
+ # @return [Array<Integer>] Binary encoded update
205
+ # @!visibility private
206
+
207
+ # @!method ydoc_transact
208
+ # Creates a new transaction for the document
209
+ #
210
+ # @example Create transaction on doc
211
+ # doc = Y::Doc.new
212
+ # tx = doc.ydoc_transact
213
+ #
214
+ # @return [Y::Transaction] The transaction object
215
+ # @!visibility private
216
+ end
217
+ end