y-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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