y-rb 0.6.0-x86_64-linux-gnu

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ # The Awareness class implements a simple shared state protocol that can be
5
+ # used for non-persistent data like awareness information (cursor, username,
6
+ # status, ..). Each client can update its own local state and listen to state
7
+ # changes of remote clients.
8
+ #
9
+ # Each client is identified by a unique client id (something we borrow from
10
+ # doc.client_id). A client can override its own state by propagating a message
11
+ # with an increasing timestamp (clock). If such a message is received, it is
12
+ # applied if the known state of that client is older than the new state
13
+ # (`clock < new_clock`). If a client thinks that a remote client is offline,
14
+ # it may propagate a message with `{ clock, state: null, client }`. If such a
15
+ # message is received, and the known clock of that client equals the received
16
+ # clock, it will clean the state.
17
+ #
18
+ # Before a client disconnects, it should propagate a null state with an
19
+ # updated clock.
20
+ #
21
+ # Awareness is an integral part of collaborative applications, you can read
22
+ # more about the concept here: https://docs.yjs.dev/getting-started/adding-awareness
23
+ #
24
+ # @example Instantiate awareness instance and encode update for broadcast
25
+ # local_state = {
26
+ # editing: { field: "description", pos: 0 },
27
+ # name: "Hannes Moser"
28
+ # }
29
+ #
30
+ # awareness = Y::Awareness.new
31
+ # awareness.local_state = local_state
32
+ # awareness.diff # [1,227,245,175,195,11,1,65,123, …]
33
+ #
34
+ # @example Two connected clients
35
+ # local_state_a = { name: "User A" }
36
+ #
37
+ # client_a = Y::Awareness.new
38
+ # client_a.local_state = local_state
39
+ #
40
+ # local_state_b = { name: "User B" }
41
+ #
42
+ # client_b = Y::Awareness.new
43
+ # client_b.local_state = local_state_b
44
+ #
45
+ # client_a.sync(client_b.diff)
46
+ # client_a.clients # {1242157267=>"{\"name\":\"User A\"}", 2401067547=>…
47
+ class Awareness
48
+ # Applies an incoming update. This gets the local awareness instance in
49
+ # sync with changes from another client. i.e., updates the state of another
50
+ # user in the local awareness instance.
51
+ #
52
+ # @example Apply an incoming update
53
+ # update = [1,227,245,175,195,11,1,65,123, …]
54
+ #
55
+ # awareness = Y::Awareness.new
56
+ # awareness.sync(update)
57
+ #
58
+ # @param diff [Array<Integer>] A binary encoded update
59
+ # @return [void]
60
+ def sync(diff)
61
+ yawareness_apply_update(diff)
62
+ end
63
+
64
+ # Clears out a state of a current client, effectively marking it as
65
+ # disconnected.
66
+ #
67
+ # @return [void]
68
+ def clean_local_state
69
+ yawareness_clean_local_state
70
+ end
71
+
72
+ # Returns a globally unique client ID of an underlying Doc.
73
+ #
74
+ # @return [Integer] Returns the client_id of the local user
75
+ def client_id
76
+ yawareness_client_id
77
+ end
78
+
79
+ # Returns a state map of all of the clients tracked by current Awareness
80
+ # instance. Those states are identified by their corresponding ClientIDs.
81
+ # The associated state is represented and replicated to other clients as a
82
+ # JSON string.
83
+ #
84
+ # @example Instantiate awareness instance and encode update for broadcast
85
+ # local_state = {
86
+ # editing: { field: "description", pos: 0 },
87
+ # name: "Hannes Moser"
88
+ # }
89
+ #
90
+ # awareness = Y::Awareness.new
91
+ # awareness.local_state = local_state
92
+ # awareness.clients # {312134501=>"{\"editing\":{\"field\":\"descriptio …
93
+ #
94
+ # @return [Hash] All clients and their current state
95
+ def clients
96
+ transform = yawareness_clients.map do |client_id, state|
97
+ [client_id, JSON.parse!(state)]
98
+ end
99
+ transform.to_h
100
+ end
101
+
102
+ # Returns the state of the local Awareness instance.
103
+ #
104
+ # @example Create local state and inspect it
105
+ # local_state = {
106
+ # editing: { field: "description", pos: 0 },
107
+ # name: "Hannes Moser"
108
+ # }
109
+ #
110
+ # awareness = Y::Awareness.new
111
+ # awareness.local_state = local_state
112
+ # awareness.local_state # { editing: { field: "description", ...
113
+ #
114
+ # @return [String] The current state of the local client
115
+ def local_state
116
+ json = yawareness_local_state
117
+ JSON.parse!(json) if json
118
+ end
119
+
120
+ # Sets a current Awareness instance state to a corresponding JSON string.
121
+ # This state will be replicated to other clients as part of the
122
+ # AwarenessUpdate.
123
+ #
124
+ # @example Set local state
125
+ # local_state = {
126
+ # editing: { field: "description", pos: 0 },
127
+ # name: "Hannes Moser"
128
+ # }
129
+ #
130
+ # awareness = Y::Awareness.new
131
+ # awareness.local_state = local_state
132
+ #
133
+ # @param [#to_json] state
134
+ # @return [void]
135
+ def local_state=(state)
136
+ raise "state cannot be encoded to JSON" unless state.respond_to? :to_json
137
+
138
+ yawareness_set_local_state(state.to_json)
139
+ end
140
+
141
+ # Subscribes to changes
142
+ #
143
+ # @return [Integer] The subscription ID
144
+ def attach(callback = nil, &block)
145
+ return yawareness_on_update(callback) unless callback.nil?
146
+
147
+ yawareness_on_update(block.to_proc) unless block.nil?
148
+ end
149
+
150
+ # Clears out a state of a given client, effectively marking it as
151
+ # disconnected.
152
+ #
153
+ # @param client_id [Integer] Clears the state for given client_id
154
+ # @return [void]
155
+ def remove_state(client_id)
156
+ yawareness_remove_state(client_id)
157
+ end
158
+
159
+ # Returns a serializable update object which is representation of a current
160
+ # Awareness state.
161
+ #
162
+ # @return [::Array<Integer>] Binary encoded update of the local instance
163
+ def diff
164
+ yawareness_update
165
+ end
166
+
167
+ # Returns a serializable update object which is representation of a current
168
+ # Awareness state. Unlike Awareness::update, this method variant allows to
169
+ # prepare update only for a subset of known clients. These clients must all
170
+ # be known to a current Awareness instance, otherwise a
171
+ # Error::ClientNotFound error will be returned.
172
+ #
173
+ # @param clients [::Array<Integer>] A list of client IDs
174
+ # @return [String] Binary encoded update including all given client IDs
175
+ def diff_with_clients(*clients)
176
+ yawareness_update_with_clients(clients)
177
+ end
178
+
179
+ # rubocop:disable Lint/UselessAccessModifier
180
+ private
181
+
182
+ # @!method yawareness_apply_update(update)
183
+ # Applies an update
184
+ #
185
+ # @param A [Y::AwarenessUpdate] Structure that represents an encodable state
186
+ # of an Awareness struct.
187
+ # @!visibility private
188
+
189
+ # @!method yawareness_apply_update(update)
190
+ # Applies an update
191
+ #
192
+ # @param A [Y::AwarenessUpdate] Structure that represents an encodable state
193
+ # of an Awareness struct.
194
+ # @!visibility private
195
+
196
+ # @!method yawareness_clean_local_state
197
+ # Clears out a state of a current client , effectively marking it as
198
+ # disconnected.
199
+ # @!visibility private
200
+
201
+ # @!method yawareness_client_id
202
+ # Returns a globally unique client ID of an underlying Doc.
203
+ # @return [Integer] The Client ID
204
+ # @!visibility private
205
+
206
+ # @!method yawareness_clients
207
+ # Returns a state map of all of the clients
208
+ # tracked by current Awareness instance. Those states are identified by
209
+ # their corresponding ClientIDs. The associated state is represented and
210
+ # replicated to other clients as a JSON string.
211
+ #
212
+ # @return [Hash<Integer, String>] Map of clients
213
+ # @!visibility private
214
+
215
+ # @!method yawareness_local_state
216
+ #
217
+ # @return [String, nil] Returns a JSON string state representation of a
218
+ # current Awareness instance.
219
+ # @!visibility private
220
+
221
+ # @!method yawareness_on_update(callback, &block)
222
+ #
223
+ # @param callback [callback]
224
+ # @return [Integer] The subscription ID
225
+ # @!visibility private
226
+
227
+ # @!method yawareness_remove_on_update(subscription_id)
228
+ #
229
+ # @param subscription_id [Integer] The subscription id to remove
230
+ # @!visibility private
231
+
232
+ # @!method yawareness_remove_state(client_id)
233
+ # Clears out a state of a given client, effectively marking it as
234
+ # disconnected.
235
+ #
236
+ # @param client_id [Integer] A Client ID
237
+ # @return [String, nil] Returns a JSON string state representation of a
238
+ # current Awareness instance.
239
+ # @!visibility private
240
+
241
+ # @!method yawareness_set_local_state(state)
242
+ # Sets a current Awareness instance state to a corresponding JSON string.
243
+ # This state will be replicated to other clients as part of the
244
+ # AwarenessUpdate and it will trigger an event to be emitted if current
245
+ # instance was created using [Awareness::with_observer] method.
246
+ #
247
+ # @param Returns [String] A state map of all of the clients tracked by
248
+ # current Awareness instance. Those states are identified by their
249
+ # corresponding ClientIDs. The associated state is represented and
250
+ # replicated to other clients as a JSON string.
251
+ # @!visibility private
252
+
253
+ # @!method yawareness_update
254
+ # Returns a serializable update object which is representation of a
255
+ # current Awareness state.
256
+ #
257
+ # @return [Y::AwarenessUpdate] The update object
258
+ # @!visibility private
259
+
260
+ # @!method yawareness_update_with_clients(clients)
261
+ # Returns a serializable update object which is representation of a
262
+ # current Awareness state. Unlike [Y::Awareness#update], this method
263
+ # variant allows to prepare update only for a subset of known clients.
264
+ # These clients must all be known to a current Awareness instance,
265
+ # otherwise an error will be returned.
266
+ #
267
+ # @param clients [::Array<Integer>]
268
+ # @return [::Array<Integer>] A serialized (binary encoded) update object
269
+ # @!visibility private
270
+
271
+ # rubocop:enable Lint/UselessAccessModifier
272
+ end
273
+
274
+ # @!visibility private
275
+ class AwarenessEvent
276
+ private # rubocop:disable Lint/UselessAccessModifier
277
+
278
+ # @!method added
279
+ # @return [::Array<Integer>] Added clients
280
+ # @!visibility private
281
+
282
+ # @!method updated
283
+ # @return [::Array<Integer>] Updated clients
284
+ # @!visibility private
285
+
286
+ # @!method removed
287
+ # @return [::Array<Integer>] Removed clients
288
+ # @!visibility private
289
+ end
290
+ end
data/lib/y/diff.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Y
4
+ # A representation of an uniformly-formatted chunk of rich context stored by
5
+ # Text or XmlText. It contains a value (which could be a string, embedded
6
+ # object or another shared type) with optional formatting attributes wrapping
7
+ # around this chunk.
8
+ class Diff
9
+ # @return [Object]
10
+ def insert
11
+ ydiff_insert
12
+ end
13
+
14
+ # @return [Hash]
15
+ def attrs
16
+ ydiff_attrs
17
+ end
18
+
19
+ # Convert the diff to a Hash representation
20
+ #
21
+ # @return [Hash]
22
+ def to_h
23
+ {
24
+ insert: ydiff_insert,
25
+ attrs: ydiff_attrs
26
+ }
27
+ end
28
+
29
+ # @!method ydiff_insert()
30
+ # Returns string representation of text
31
+ #
32
+ # @return [Object]
33
+
34
+ # @!method ydiff_attrs()
35
+ #
36
+ # @return [Hash]
37
+ end
38
+ end
data/lib/y/doc.rb ADDED
@@ -0,0 +1,313 @@
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
+ ZERO_STATE = [0].freeze
20
+ private_constant :ZERO_STATE
21
+
22
+ ZERO_STATE_V2 = [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0].freeze
23
+ private_constant :ZERO_STATE_V2
24
+
25
+ # Attach a listener to document changes. If one of the data structures is
26
+ # changes, the block is called with the update as its only argument.
27
+ #
28
+ # @yield [update] Called when document is updated
29
+ # @yieldparam [Array<Integer>] update The encoded document updates
30
+
31
+ # Example: Attach listener to document changes
32
+ # doc = described_class.new
33
+ # doc.attach { |update| pp update }
34
+ #
35
+ # text = doc.get_text("my text")
36
+ # text << "1"
37
+ def attach(&block)
38
+ ydoc_observe_update(block)
39
+ end
40
+
41
+ # Commit current transaction
42
+ #
43
+ # This is a convenience method that invokes {Y::Transaction#commit} on the
44
+ # current transaction used by this document.
45
+ #
46
+ # @return [void]
47
+ def commit
48
+ current_transaction(&:commit)
49
+ end
50
+
51
+ # Create a diff between this document and another document. The diff is
52
+ # created based on a state vector provided by the other document. It only
53
+ # returns the missing blocks, as binary encoded sequence.
54
+ #
55
+ # @param state [::Array<Integer>] The state to create the diff against
56
+ # @return [::Array<Integer>] Binary encoded diff
57
+ def diff(state = ZERO_STATE)
58
+ current_transaction { |tx| ydoc_encode_diff_v1(tx, state) }
59
+ end
60
+
61
+ # Create a v2 diff between this document and another document. The diff is
62
+ # created based on a state vector provided by the other document. It only
63
+ # returns the missing blocks, as binary encoded sequence.
64
+ #
65
+ # @param state [::Array<Integer>] The state to create the diff against
66
+ # @return [::Array<Integer>] Binary encoded diff
67
+ def diff_v2(state = ZERO_STATE_V2)
68
+ current_transaction { |tx| ydoc_encode_diff_v2(tx, state) }
69
+ end
70
+
71
+ # Creates a full diff for the current document. It is similar to {#diff},
72
+ # but does not take a state. Instead it creates an empty state and passes it
73
+ # to the encode_diff function.
74
+ #
75
+ # @return [::Array<Integer>] Binary encoded diff
76
+ def full_diff
77
+ diff
78
+ end
79
+
80
+ # Gets or creates a new array by name
81
+ #
82
+ # If the optional values array is present, fills the array up with elements
83
+ # from the provided array. If the array already exists and isn't
84
+ # empty, elements are pushed to the end of the array.
85
+ #
86
+ # @param name [String] The name of the structure
87
+ # @param values [::Array] Optional initial values
88
+ # @return [Y::Array]
89
+ def get_array(name, values = nil)
90
+ array = ydoc_get_or_insert_array(name)
91
+ array.document = self
92
+ array.concat(values) unless values.nil?
93
+ array
94
+ end
95
+
96
+ # Gets or creates a new map by name
97
+ #
98
+ # If the optional input hash is present, fills the map up with key-value
99
+ # pairs from the provided input hash. If the map already exists and isn't
100
+ # empty, any existing keys are overridden and new keys are added.
101
+ #
102
+ # @param name [String] The name of the structure
103
+ # @param input [Hash] Optional initial map key-value pairs
104
+ # @return [Y::Map]
105
+ def get_map(name, input = nil)
106
+ map = ydoc_get_or_insert_map(name)
107
+ map.document = self
108
+ input&.each { |key, value| map[key] = value }
109
+ map
110
+ end
111
+
112
+ # Gets or creates a new text by name
113
+ #
114
+ # If the optional input string is provided, fills a new text with the string
115
+ # at creation time. If the text isn't new and not empty, appends the input
116
+ # to the end of the text.
117
+ #
118
+ # @param name [String] The name of the structure
119
+ # @param input [String] Optional initial text value
120
+ # @return [Y::Text]
121
+ def get_text(name, input = nil)
122
+ text = ydoc_get_or_insert_text(name)
123
+ text.document = self
124
+ text << input unless input.nil?
125
+ text
126
+ end
127
+
128
+ # Gets or creates a new XMLElement by name
129
+ #
130
+ # @param name [String] The name of the structure
131
+ # @return [Y::XMLElement]
132
+ def get_xml_element(name)
133
+ xml_element = ydoc_get_or_insert_xml_element(name)
134
+ xml_element.document = self
135
+ xml_element
136
+ end
137
+
138
+ # Gets or creates a new XMLFragment by name
139
+ #
140
+ # @param name [String] The name of the fragment
141
+ # @return [Y::XMLFragment]
142
+ def get_xml_fragment(name)
143
+ xml_fragment = ydoc_get_or_insert_xml_fragment(name)
144
+ xml_fragment.document = self
145
+ xml_fragment
146
+ end
147
+
148
+ # Gets or creates a new XMLText by name
149
+ #
150
+ # @param name [String] The name of the structure
151
+ # @param input [String] Optional initial text value
152
+ # @return [Y::XMLText]
153
+ def get_xml_text(name, input = nil)
154
+ xml_text = ydoc_get_or_insert_xml_text(name)
155
+ xml_text.document = self
156
+ xml_text << input unless input.nil?
157
+ xml_text
158
+ end
159
+
160
+ # Creates a state vector of this document. This can be used to compare the
161
+ # state of two documents with each other and to later on sync them.
162
+ #
163
+ # @return [::Array<Integer>] Binary encoded state vector
164
+ def state
165
+ current_transaction(&:state)
166
+ end
167
+
168
+ # Creates a v2 state vector of this document. This can be used to compare
169
+ # the state of two documents with each other and to later on sync them.
170
+ #
171
+ # @return [::Array<Integer>] Binary encoded state vector
172
+ def state_v2
173
+ current_transaction(&:state_v2)
174
+ end
175
+
176
+ # Synchronizes this document with the diff from another document
177
+ #
178
+ # @param diff [::Array<Integer>] Binary encoded update
179
+ # @return [void]
180
+ def sync(diff)
181
+ current_transaction { |tx| tx.apply(diff) }
182
+ end
183
+
184
+ # Synchronizes this document with the v2 diff from another document
185
+ #
186
+ # @param diff [::Array<Integer>] Binary encoded update
187
+ # @return [void]
188
+ def sync_v2(diff)
189
+ current_transaction { |tx| tx.apply_v2(diff) }
190
+ end
191
+
192
+ # Restores a specific document from an update that contains full state
193
+ #
194
+ # This is doing the same as {#sync}, but it exists to be explicit about
195
+ # the intent. This is the companion to {#full_diff}.
196
+ #
197
+ # @param full_diff [::Array<Integer>] Binary encoded update
198
+ # @return [void]
199
+ def restore(full_diff)
200
+ current_transaction { |tx| tx.apply(full_diff) }
201
+ end
202
+
203
+ # Creates a new transaction
204
+ def transact
205
+ # 1. release potentially existing transaction
206
+ if @current_transaction
207
+ @current_transaction.free
208
+ @current_transaction = nil
209
+ end
210
+
211
+ # 2. store new transaction in instance variable
212
+ @current_transaction = ydoc_transact
213
+ @current_transaction.document = self
214
+
215
+ # 3. call block with reference to current_transaction
216
+ yield @current_transaction
217
+ ensure
218
+ @current_transaction&.free
219
+ @current_transaction = nil
220
+ end
221
+
222
+ # @!visibility private
223
+ def current_transaction(&block)
224
+ raise "provide a block" unless block
225
+
226
+ # 1. instance variable is set, just use it
227
+ return yield @current_transaction if @current_transaction
228
+
229
+ # 2. forward block to transact
230
+ transact(&block) unless @current_transaction
231
+ end
232
+
233
+ # @!method ydoc_encode_diff_v1(tx, state_vector)
234
+ # Encodes the diff of current document state vs provided state
235
+ #
236
+ # @example Create transaction on doc
237
+ # doc = Y::Doc.new
238
+ # tx = doc.ydoc_encode_diff_v1(other_state)
239
+ #
240
+ # @return [Array<Integer>] Binary encoded update
241
+ # @!visibility private
242
+
243
+ # @!method ydoc_encode_diff_v2(tx, state_vector)
244
+ # Encodes the diff of current document state vs provided state in the v2
245
+ # format
246
+ #
247
+ # @example Create transaction on doc
248
+ # doc = Y::Doc.new
249
+ # tx = doc.ydoc_encode_diff_v2(other_state)
250
+ #
251
+ # @return [Array<Integer>] Binary encoded update
252
+ # @!visibility private
253
+
254
+ # @!method ydoc_transact
255
+ # Creates a new transaction for the document
256
+ #
257
+ # @example Create transaction on doc
258
+ # doc = Y::Doc.new
259
+ # tx = doc.ydoc_transact
260
+ #
261
+ # @return [Y::Transaction] The transaction object
262
+ # @!visibility private
263
+
264
+ # @!method ydoc_get_or_insert_array(name)
265
+ # Creates a new array for the document
266
+ #
267
+ # @param [String] name
268
+ # @return [Y::Array]
269
+ # @!visibility private
270
+
271
+ # @!method ydoc_get_or_insert_map(name)
272
+ # Creates a new map for the document
273
+ #
274
+ # @param [String] name
275
+ # @return [Y::Map]
276
+ # @!visibility private
277
+
278
+ # @!method ydoc_get_or_insert_text(name)
279
+ # Creates a new text for the document
280
+ #
281
+ # @param [String] name
282
+ # @return [Y::Text]
283
+ # @!visibility private
284
+
285
+ # @!method ydoc_get_or_insert_xml_element(name)
286
+ # Creates a new XMLText for the document
287
+ #
288
+ # @param [String] name
289
+ # @return [Y::XMLElement]
290
+ # @!visibility private
291
+
292
+ # @!method ydoc_get_or_insert_xml_fragment(name)
293
+ # Creates a new XMLFragment for the document
294
+ #
295
+ # @param [String] name
296
+ # @return [Y::XMLFragment]
297
+ # @!visibility private
298
+
299
+ # @!method ydoc_get_or_insert_xml_text(name)
300
+ # Creates a new XMLText for the document
301
+ #
302
+ # @param [String] name
303
+ # @return [Y::XMLText]
304
+ # @!visibility private
305
+
306
+ # @!method ydoc_observe_update(block)
307
+ # Creates a subscription to observe changes to the document
308
+ #
309
+ # @param [Proc] block
310
+ # @return [Integer]
311
+ # @!visibility private
312
+ end
313
+ end