y-rb 0.3.2-x64-mingw32

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,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