y-rb 0.3.2-x64-mingw32

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,266 @@
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
+ # }.to_json
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
+ #
35
+ class Awareness
36
+ # Applies an incoming update. This gets the local awareness instance in
37
+ # sync with changes from another client. i.e., updates the state of another
38
+ # user in the local awareness instance.
39
+ #
40
+ # @example Apply an incoming update
41
+ # update = [1,227,245,175,195,11,1,65,123, …]
42
+ #
43
+ # awareness = Y::Awareness.new
44
+ # awareness.sync(update)
45
+ #
46
+ # @param [Array<Integer>] diff A binary encoded update
47
+ # @return [void]
48
+ def sync(diff)
49
+ yawareness_apply_update(diff)
50
+ end
51
+
52
+ # Clears out a state of a current client, effectively marking it as
53
+ # disconnected.
54
+ #
55
+ # @return [void]
56
+ def clean_local_state
57
+ yawareness_clean_local_state
58
+ end
59
+
60
+ # Returns a globally unique client ID of an underlying Doc.
61
+ #
62
+ # @return [Integer] Returns the client_id of the local user
63
+ def client_id
64
+ yawareness_client_id
65
+ end
66
+
67
+ # Returns a state map of all of the clients tracked by current Awareness
68
+ # instance. Those states are identified by their corresponding ClientIDs.
69
+ # The associated state is represented and replicated to other clients as a
70
+ # JSON string.
71
+ #
72
+ # @example Instantiate awareness instance and encode update for broadcast
73
+ # local_state = {
74
+ # editing: { field: "description", pos: 0 },
75
+ # name: "Hannes Moser"
76
+ # }.to_json
77
+ #
78
+ # awareness = Y::Awareness.new
79
+ # awareness.local_state = local_state
80
+ # awareness.clients # {312134501=>"{\"editing\":{\"field\":\"descriptio …
81
+ #
82
+ # @return [Hash] All clients and their current state
83
+ def clients
84
+ yawareness_clients
85
+ end
86
+
87
+ # Returns a JSON string state representation of a current Awareness
88
+ # instance.
89
+ #
90
+ # @example Create local state and inspect it
91
+ # local_state = {
92
+ # editing: { field: "description", pos: 0 },
93
+ # name: "Hannes Moser"
94
+ # }.to_json
95
+ #
96
+ # awareness = Y::Awareness.new
97
+ # awareness.local_state = local_state
98
+ # local_state # "{\"editing\":{\"field\":\"description\",\"pos\":0}, …
99
+ #
100
+ # @return [String] The current state of the local client
101
+ def local_state
102
+ yawareness_local_state
103
+ end
104
+
105
+ # Sets a current Awareness instance state to a corresponding JSON string.
106
+ # This state will be replicated to other clients as part of the
107
+ # AwarenessUpdate.
108
+ #
109
+ # @example Set local state
110
+ # local_state = {
111
+ # editing: { field: "description", pos: 0 },
112
+ # name: "Hannes Moser"
113
+ # }.to_json
114
+ #
115
+ # awareness = Y::Awareness.new
116
+ # awareness.local_state = local_state
117
+ #
118
+ # @return [void]
119
+ def local_state=(json)
120
+ yawareness_set_local_state(json)
121
+ end
122
+
123
+ # Subscribes to changes
124
+ #
125
+ # @return [Integer] The subscription ID
126
+ def attach(callback, &block)
127
+ return yawareness_on_update(callback) unless callback.nil?
128
+
129
+ yawareness_on_update(block.to_proc) unless block.nil?
130
+ end
131
+
132
+ # Unsubscribe from changes
133
+ #
134
+ # @param [Integer] subscription_id
135
+ # @return [void]
136
+ def detach(subscription_id)
137
+ yawareness_remove_on_update(subscription_id)
138
+ end
139
+
140
+ # Clears out a state of a given client, effectively marking it as
141
+ # disconnected.
142
+ #
143
+ # @param [Integer] client_id Clears the state for given client_id
144
+ # @return [void]
145
+ def remove_state(client_id)
146
+ yawareness_remove_state(client_id)
147
+ end
148
+
149
+ # Returns a serializable update object which is representation of a current
150
+ # Awareness state.
151
+ #
152
+ # @return [::Array<Integer>] Binary encoded update of the local instance
153
+ def diff
154
+ yawareness_update
155
+ end
156
+
157
+ # Returns a serializable update object which is representation of a current
158
+ # Awareness state. Unlike Awareness::update, this method variant allows to
159
+ # prepare update only for a subset of known clients. These clients must all
160
+ # be known to a current Awareness instance, otherwise a
161
+ # Error::ClientNotFound error will be returned.
162
+ #
163
+ # @param [::Array<Integer>] clients A list of client IDs
164
+ # @return [String] Binary encoded update including all given client IDs
165
+ def diff_with_clients(*clients)
166
+ yawareness_update_with_clients(clients)
167
+ end
168
+
169
+ # rubocop:disable Lint/UselessAccessModifier
170
+ private
171
+
172
+ # @!method yawareness_apply_update(update)
173
+ # Applies an update
174
+ #
175
+ # @param [Y::AwarenessUpdate] A structure that represents an encodable state
176
+ # of an Awareness struct.
177
+
178
+ # @!method yawareness_apply_update(update)
179
+ # Applies an update
180
+ #
181
+ # @param [Y::AwarenessUpdate] A structure that represents an encodable state
182
+ # of an Awareness struct.
183
+
184
+ # @!method yawareness_clean_local_state
185
+ # Clears out a state of a current client , effectively marking it as
186
+ # disconnected.
187
+
188
+ # @!method yawareness_client_id
189
+ # Returns a globally unique client ID of an underlying Doc.
190
+ # @return [Integer] The Client ID
191
+
192
+ # @!method yawareness_clients
193
+ # Returns a state map of all of the clients
194
+ # tracked by current Awareness instance. Those states are identified by
195
+ # their corresponding ClientIDs. The associated state is represented and
196
+ # replicated to other clients as a JSON string.
197
+ #
198
+ # @return [Hash<Integer, String>] Map of clients
199
+
200
+ # @!method yawareness_local_state
201
+ #
202
+ # @return [String|nil] Returns a JSON string state representation of a
203
+ # current Awareness instance.
204
+
205
+ # @!method yawareness_on_update(callback, &block)
206
+ #
207
+ # @param [Proc] A callback handler for updates
208
+ # @return [Integer] The subscription ID
209
+
210
+ # @!method yawareness_remove_on_update(subscription_id)
211
+ #
212
+ # @param [Integer] subscription_id The subscription id to remove
213
+
214
+ # @!method yawareness_remove_state(client_id)
215
+ # Clears out a state of a given client, effectively marking it as
216
+ # disconnected.
217
+ #
218
+ # @param [Integer] client_id A Client ID
219
+ # @return [String|nil] Returns a JSON string state representation of a
220
+ # current Awareness instance.
221
+
222
+ # @!method yawareness_set_local_state(state)
223
+ # Sets a current Awareness instance state to a corresponding JSON string.
224
+ # This state will be replicated to other clients as part of the
225
+ # AwarenessUpdate and it will trigger an event to be emitted if current
226
+ # instance was created using [Awareness::with_observer] method.
227
+ #
228
+ # @param [String] Returns a state map of all of the clients tracked by
229
+ # current Awareness instance. Those states are identified by their
230
+ # corresponding ClientIDs. The associated state is represented and
231
+ # replicated to other clients as a JSON string.
232
+
233
+ # @!method yawareness_update
234
+ # Returns a serializable update object which is representation of a
235
+ # current Awareness state.
236
+ #
237
+ # @return [Y::AwarenessUpdate] The update object
238
+
239
+ # @!method yawareness_update_with_clients(clients)
240
+ # Returns a serializable update object which is representation of a
241
+ # current Awareness state. Unlike [Y::Awareness#update], this method
242
+ # variant allows to prepare update only for a subset of known clients.
243
+ # These clients must all be known to a current Awareness instance,
244
+ # otherwise an error will be returned.
245
+ #
246
+ # @param [::Array<Integer>] clients
247
+ # @return [::Array<Integer>] A serialized (binary encoded) update object
248
+
249
+ # rubocop:enable Lint/UselessAccessModifier
250
+ end
251
+
252
+ # rubocop:disable Lint/UselessAccessModifier
253
+ class AwarenessEvent
254
+ private
255
+
256
+ # @!method added
257
+ # @return [::Array<Integer>] Added clients
258
+
259
+ # @!method updated
260
+ # @return [::Array<Integer>] Updated clients
261
+
262
+ # @!method removed
263
+ # @return [::Array<Integer>] Removed clients
264
+ end
265
+ # rubocop:enable Lint/UselessAccessModifier
266
+ end
data/lib/y/doc.rb ADDED
@@ -0,0 +1,220 @@
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
+ # Commit current transaction
23
+ #
24
+ # This is a convenience method that invokes {Y::Transaction#commit} on the
25
+ # current transaction used by this document.
26
+ #
27
+ # @return [void]
28
+ def commit
29
+ current_transaction.commit
30
+ end
31
+
32
+ # The currently active transaction for this document
33
+ # @return [Y::Transaction]
34
+ def current_transaction
35
+ @current_transaction ||= begin
36
+ transaction = ydoc_transact
37
+ transaction.document = self
38
+ transaction
39
+ end
40
+ end
41
+
42
+ # Create a diff between this document and another document. The diff is
43
+ # created based on a state vector provided by the other document. It only
44
+ # returns the missing blocks, as binary encoded sequence.
45
+ #
46
+ # @param [::Array<Int>] state The state to create the diff against
47
+ # @return [::Array<Int>] Binary encoded diff
48
+ def diff(state = ZERO_STATE)
49
+ ydoc_encode_diff_v1(state)
50
+ end
51
+
52
+ # Creates a full diff for the current document. It is similar to {#diff},
53
+ # but does not take a state. Instead it creates an empty state and passes it
54
+ # to the encode_diff function.
55
+ #
56
+ # @return [::Array<Int>] Binary encoded diff
57
+ def full_diff
58
+ empty_state = Y::Doc.new.state
59
+ ydoc_encode_diff_v1(empty_state)
60
+ end
61
+
62
+ # Gets or creates a new array by name
63
+ #
64
+ # If the optional values array is present, fills the array up with elements
65
+ # from the provided array. If the array already exists and isn't
66
+ # empty, elements are pushed to the end of the array.
67
+ #
68
+ # @param [String] name The name of the structure
69
+ # @param [::Array] values Optional initial values
70
+ # @return [Y::Array]
71
+ def get_array(name, values = nil)
72
+ array = current_transaction.get_array(name)
73
+ array.document = self
74
+ array.concat(values) unless values.nil?
75
+ array
76
+ end
77
+
78
+ # Gets or creates a new map by name
79
+ #
80
+ # If the optional input hash is present, fills the map up with key-value
81
+ # pairs from the provided input hash. If the map already exists and isn't
82
+ # empty, any existing keys are overridden and new keys are added.
83
+ #
84
+ # @param [String] name The name of the structure
85
+ # @param [Hash] input Optional initial map key-value pairs
86
+ # @return [Y::Map]
87
+ def get_map(name, input = nil)
88
+ map = current_transaction.get_map(name)
89
+ map.document = self
90
+ input&.each { |key, value| map[key] = value }
91
+ map
92
+ end
93
+
94
+ # Gets or creates a new text by name
95
+ #
96
+ # If the optional input string is provided, fills a new text with the string
97
+ # at creation time. If the text isn't new and not empty, appends the input
98
+ # to the end of the text.
99
+ #
100
+ # @param [String] name The name of the structure
101
+ # @param [String] input Optional initial text value
102
+ # @return [Y::Text]
103
+ def get_text(name, input = nil)
104
+ text = current_transaction.get_text(name)
105
+ text.document = self
106
+ text << input unless input.nil?
107
+ text
108
+ end
109
+
110
+ # Gets or creates a new XMLElement by name
111
+ #
112
+ # @param [String] name The name of the structure
113
+ # @return [Y::XMLElement]
114
+ def get_xml_element(name)
115
+ xml_element = current_transaction.get_xml_element(name)
116
+ xml_element&.document = self
117
+ xml_element
118
+ end
119
+
120
+ # Gets or creates a new XMLText by name
121
+ #
122
+ # @param [String] name The name of the structure
123
+ # @param [String] input Optional initial text value
124
+ # @return [Y::XMLText]
125
+ def get_xml_text(name, input = nil)
126
+ xml_text = current_transaction.get_xml_text(name)
127
+ xml_text.document = self
128
+ xml_text << input unless input.nil?
129
+ xml_text
130
+ end
131
+
132
+ # Creates a state vector of this document. This can be used to compare the
133
+ # state of two documents with each other and to later on sync them.
134
+ #
135
+ # @return [::Array<Int>] Binary encoded state vector
136
+ def state
137
+ current_transaction.state
138
+ end
139
+
140
+ # Synchronizes this document with the diff from another document
141
+ #
142
+ # @param [::Array<Int>] diff Binary encoded update
143
+ # @return [void]
144
+ def sync(diff)
145
+ current_transaction.apply(diff)
146
+ end
147
+
148
+ # Restores a specific document from an update that contains full state
149
+ #
150
+ # This is doing the same as {#sync}, but it exists to be explicit about
151
+ # the intent. This is the companion to {#full_diff}.
152
+ #
153
+ # @param [::Array<Int>] full_diff Binary encoded update
154
+ # @return [void]
155
+ def restore(full_diff)
156
+ current_transaction.apply(full_diff)
157
+ end
158
+
159
+ # rubocop:disable Metrics/MethodLength
160
+
161
+ # Creates a new transaction and provides it to the given block
162
+ #
163
+ # @example Insert into text
164
+ # doc = Y::Doc.new
165
+ # text = doc.get_text("my text")
166
+ #
167
+ # doc.transact do
168
+ # text << "Hello, World!"
169
+ # end
170
+ #
171
+ # @yield [transaction]
172
+ # @yieldparam [Y::Transaction] transaction
173
+ # @yieldreturn [void]
174
+ # @return [Y::Transaction]
175
+ def transact
176
+ current_transaction.commit
177
+
178
+ if block_given?
179
+ # create new transaction just for the lifetime of this block
180
+ tmp_transaction = ydoc_transact
181
+ tmp_transaction.document = self
182
+
183
+ # override transaction for the lifetime of the block
184
+ @current_transaction = tmp_transaction
185
+
186
+ yield tmp_transaction
187
+
188
+ tmp_transaction.commit
189
+ end
190
+
191
+ # create new transaction
192
+ @current_transaction = ydoc_transact
193
+ @current_transaction.document = self
194
+
195
+ current_transaction
196
+ end
197
+
198
+ # rubocop:enable Metrics/MethodLength
199
+
200
+ # @!method ydoc_encode_diff_v1
201
+ # Encodes the diff of current document state vs provided state
202
+ #
203
+ # @example Create transaction on doc
204
+ # doc = Y::Doc.new
205
+ # tx = doc.ydoc_encode_diff_v1(other_state)
206
+ #
207
+ # @return [Array<Integer>] Binary encoded update
208
+ # @!visibility private
209
+
210
+ # @!method ydoc_transact
211
+ # Creates a new transaction for the document
212
+ #
213
+ # @example Create transaction on doc
214
+ # doc = Y::Doc.new
215
+ # tx = doc.ydoc_transact
216
+ #
217
+ # @return [Y::Transaction] The transaction object
218
+ # @!visibility private
219
+ end
220
+ end
data/lib/y/map.rb ADDED
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Y
6
+ # A map can be used to store and retrieve key-value pairs.
7
+ #
8
+ # The map is the replicated counterpart to a Hash. It supports a subset
9
+ # of the Hash operations, like adding, getting and deleting values by key.
10
+ #
11
+ # Someone should not instantiate a map directly, but use {Y::Doc#get_map}
12
+ # instead.
13
+ #
14
+ # @example
15
+ # doc = Y::Doc.new
16
+ # map = doc.get_map("my map")
17
+ #
18
+ # map[:hello] = "world"
19
+ # puts map[:hello]
20
+ class Map
21
+ include Enumerable
22
+
23
+ # @!attribute [r] document
24
+ #
25
+ # @return [Y::Doc] The document this map belongs to
26
+ attr_accessor :document
27
+
28
+ # Create a new map 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
+ # Attach a listener to get notified about any changes to the map
38
+ #
39
+ # @param [Proc] callback
40
+ # @param [Block] block
41
+ # @return [Integer]
42
+ def attach(callback, &block)
43
+ return ymap_observe(callback) unless callback.nil?
44
+
45
+ ymap_observe(block.to_proc) unless block.nil?
46
+ end
47
+
48
+ # Removes all map entries
49
+ #
50
+ # @return [Self]
51
+ def clear
52
+ ymap_clear(transaction)
53
+ self
54
+ end
55
+
56
+ # rubocop:disable Layout/LineLength
57
+
58
+ # Deletes the entry for the given key and returns its associated value.
59
+ #
60
+ # @example Deletes the entry and return associated value
61
+ #
62
+ # m = doc.get_map("my map")
63
+ # m[:bar] = 1
64
+ # m.delete(:bar) # => 1
65
+ # m # => {}
66
+ #
67
+ # @example Unknown key is handled in block
68
+ #
69
+ # m = doc.get_map("my map")
70
+ # m.delete(:nosuch) { |key| "Key #{key} not found" }# => "Key nosuch not found"
71
+ # m # => {}
72
+ #
73
+ # @param [String, Symbol] key
74
+ # @return [void]
75
+ def delete(key)
76
+ value = ymap_remove(transaction, key)
77
+ if block_given? && key?(key)
78
+ yield key
79
+ else
80
+ value
81
+ end
82
+ end
83
+
84
+ # rubocop:enable Layout/LineLength
85
+
86
+ # Detach listener
87
+ #
88
+ # @param [Integer] subscription_id
89
+ # @return [void]
90
+ def detach(subscription_id)
91
+ ymap_unobserve(subscription_id)
92
+ end
93
+
94
+ # @return [void]
95
+ def each(&block)
96
+ ymap_each(block)
97
+ end
98
+
99
+ # @return [true|false]
100
+ def key?(key)
101
+ ymap_contains(key)
102
+ end
103
+
104
+ alias has_key? key?
105
+
106
+ # @return [Object]
107
+ def [](key)
108
+ ymap_get(key)
109
+ end
110
+
111
+ # @return [void]
112
+ def []=(key, val)
113
+ ymap_insert(transaction, key, val)
114
+ end
115
+
116
+ # Returns size of map
117
+ #
118
+ # @return [Integer]
119
+ def size
120
+ ymap_size
121
+ end
122
+
123
+ # Returns a Hash representation of this map
124
+ #
125
+ # @return [Hash]
126
+ def to_h
127
+ ymap_to_h
128
+ end
129
+
130
+ # Returns a JSON representation of map
131
+ #
132
+ # @return [String] JSON string
133
+ def to_json(*_args)
134
+ to_h.to_json
135
+ end
136
+
137
+ private
138
+
139
+ # @!method ymap_clear()
140
+ # Removes all key-value pairs from Map
141
+
142
+ # @!method ymap_contains(key)
143
+ # Check if a certain key is in the Map
144
+ #
145
+ # @param [String|Symbol] key
146
+ # @return [Boolean] True, if and only if the key exists
147
+
148
+ # @!method ymap_each(proc)
149
+ # Iterates over all key-value pairs in Map by calling the provided proc
150
+ # with the key and the value as arguments.
151
+ #
152
+ # @param [Proc<String, Any>] proc A proc that is called for every element
153
+
154
+ # @!method ymap_get(key)
155
+ # Returns stored value for key or nil if none is present
156
+ #
157
+ # @param [String|Symbol] key
158
+ # @return [Any|Nil] Value or nil
159
+
160
+ # @!method ymap_insert(transaction, key, value)
161
+ # Insert value for key. In case the key already exists, the previous value
162
+ # will be overwritten.
163
+ #
164
+ # @param [Y::Transaction] transaction
165
+ # @param [String|Symbol] key
166
+ # @param [Any] value
167
+
168
+ # @!method ymap_observe(callback)
169
+ #
170
+ # @param [Proc] callback
171
+ # @return [Integer]
172
+
173
+ # @!method ymap_remove(transaction, key)
174
+ # Removes key-value pair from Map if key exists.
175
+ #
176
+ # @param [Y::Transaction] transaction
177
+ # @param [String|Symbol] key
178
+
179
+ # @!method ymap_size()
180
+ # Returns number of key-value pairs stored in map
181
+ #
182
+ # @return [Integer] Number of key-value pairs
183
+
184
+ # @!method ymap_to_h()
185
+ # Returns a Hash representation of the Map
186
+ #
187
+ # @return [Hash] Hash representation of Map
188
+
189
+ # @!method ymap_unobserve(subscription_id)
190
+ #
191
+ # @param [Integer] subscription_id
192
+ # @return [void]
193
+
194
+ # A reference to the current active transaction of the document this map
195
+ # belongs to.
196
+ #
197
+ # @return [Y::Transaction] A transaction object
198
+ def transaction
199
+ document.current_transaction
200
+ end
201
+ end
202
+ end