deepstream 0.1.7 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e408d9efd700aa2a8ab393eb87f0e00dba1e43a
4
- data.tar.gz: 812bb8d7c5af04a63334d7826701df0d961edf6b
3
+ metadata.gz: 6cd95fc9f274116e724233dc418e5341bb600504
4
+ data.tar.gz: 2ba1ca7d05528b7221f264ed6ff133a10ca26425
5
5
  SHA512:
6
- metadata.gz: 5468d8e34eed668c48c396622422884dc0fcf551e2c258b2ea2bab8a19f1ecfb8b1f9e9cd5cc226020608af59ba482da3a16ac0d4320612f497f128ff994d315
7
- data.tar.gz: 59d69c20662f95e057f7a0cd3951e31f1cd82f78a18def3976c8cce9829baeb3d91e027c32a14e2d8b880ebb7af6a5e2f60fab08d88b12ed1674a924c0f32f6c
6
+ metadata.gz: 4e8b66da27b17102bd5b52acb98c72638f95df219c767c2b4d5029ca7695f33084c74194cd453bfc2ba3e54d76b79085fc87d1c7e7f667e46c99badd449bb356
7
+ data.tar.gz: 7d0196f7f8afc7be326827ef1b93496cc2066374ed4f60279ae58e01863eb6bfaa51be66403ce990d9d30a8a809145ebd8c78686f2c909cae459d2afe0c92b9c
@@ -0,0 +1,3 @@
1
+ [submodule "test/features"]
2
+ path = test/features
3
+ url = https://github.com/deepstreamIO/deepstream.io-client-specs.git
data/README.md CHANGED
@@ -18,7 +18,7 @@ ds.emit 'my_event'
18
18
  # or
19
19
  ds.emit 'my_event', foo: 'bar', bar: 'foo'
20
20
  # or
21
- ds.emit 'my_event', { foo: 'bar', bar: 'foo' }, timeout: 3
21
+ ds.emit 'my_event', {foo: 'bar', bar: 'foo'}, timeout: 3
22
22
  # or
23
23
  ds.emit 'my_event', nil, timeout: 3
24
24
 
@@ -41,7 +41,7 @@ foo.bar = 'bar'
41
41
  foo.set('bar', 'bar')
42
42
 
43
43
  # Set the whole record
44
- foo.set(foo: 'foo', bar: 1, )
44
+ foo.set(foo: 'foo', bar: 1)
45
45
 
46
46
  # Get a list
47
47
  foo = ds.get_list('bar')
@@ -53,7 +53,7 @@ foo.add('foo')
53
53
  foo.remove('foo')
54
54
 
55
55
  # Show record names on the list
56
- foo.keys()
56
+ foo.keys
57
57
 
58
58
  # Access records on the list
59
- foo.all()
59
+ foo.all
@@ -0,0 +1,7 @@
1
+ require 'cucumber/rake/task'
2
+
3
+ Cucumber::Rake::Task.new(:test) do |t|
4
+ t.cucumber_opts = "--guess -r test/ test/features/"
5
+ end
6
+
7
+ task default: :test
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "deepstream"
7
- spec.version = "0.1.7"
7
+ spec.version = "0.2.0"
8
8
  spec.authors = ["Currency-One S.A."]
9
9
  spec.email = ["piotr.szczudlak@currency-one.com"]
10
10
 
@@ -13,8 +13,16 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/Currency-One/deepstream-ruby"
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.files += Dir['lib/**/*.rb']
16
17
  spec.bindir = "exe"
17
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
19
  spec.require_paths = ["lib"]
20
+ spec.required_ruby_version = '>= 2.3.0'
19
21
  spec.license = "Apache-2.0"
22
+ spec.add_runtime_dependency 'celluloid-websocket-client', '~> 0'
23
+ spec.add_development_dependency 'cucumber'
24
+ spec.add_development_dependency 'reel'
25
+ spec.add_development_dependency 'pry'
26
+ spec.add_development_dependency 'rspec-expectations'
27
+ spec.add_development_dependency 'rake'
20
28
  end
@@ -11,298 +11,4 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
 
14
- require 'socket'
15
- require 'json'
16
- require 'timeout'
17
-
18
- module Deepstream end
19
-
20
- class Deepstream::Record
21
- def initialize(client, name, data, version)
22
- @client, @name, @data, @version = client, name, data, version
23
- end
24
-
25
- def get_name
26
- @name
27
- end
28
-
29
- def set(*args)
30
- if args.size == 1
31
- if @client._write('R', 'U', @name, (@version += 1), JSON.dump(args[0]))
32
- @data = OpenStruct.new(args[0])
33
- end
34
- else
35
- @client._write('R', 'P', @name, (@version += 1), args[0][0..-2], @client._typed(args[1]))
36
- @data[args[0][0..-2]] = args[1]
37
- end
38
- rescue => e
39
- print "unable to set\n"
40
- print "Error: ", e.message, "\n" if @client.verbose
41
- end
42
-
43
- def _patch(version, field, value)
44
- @version = version.to_i
45
- @data[field] = value
46
- end
47
-
48
- def _update(version, data)
49
- @version = version.to_i
50
- @data = data
51
- end
52
-
53
- def method_missing(name, *args)
54
- unless @data.is_a?(Array)
55
- set(name, *args) if name[-1] == '='
56
- @data[name] || @data[name[-1]]
57
- end
58
- end
59
-
60
- def inspect
61
- "Deepstream::Record (#{@version}) #{@name} #{@data.to_h}"
62
- end
63
- end
64
-
65
- class Deepstream::List < Deepstream::Record
66
- def add(record_name)
67
- @data = [] unless @data.is_a?(Array)
68
- unless @data.include? record_name
69
- @data.push record_name
70
- @client._write('R', 'U', @name, (@version += 1), JSON.dump(@data))
71
- end
72
- @data
73
- rescue => e
74
- print "unable to add ", @data.pop, "\n"
75
- print "Error: ", e.message, "\n" if @client.verbose
76
- @data
77
- end
78
-
79
- def remove(record_name)
80
- @data.delete_if { |x| x == record_name }
81
- @client._write('R', 'U', @name, (@version += 1), JSON.dump(@data))
82
- @data
83
- rescue => e
84
- print "unable to remove ", record_name, "\n"
85
- @data.push record_name
86
- print "Error: ", e.message, "\n" if @client.verbose
87
- @data
88
- end
89
-
90
- def all
91
- @data.map { |x| @client.get_record(x) }
92
- end
93
-
94
- def keys
95
- @data
96
- end
97
-
98
- def set(*args)
99
- fail 'cannot use set on a list'
100
- end
101
-
102
- def inspect
103
- "Deepstream::List (#{@version}) #{@name} keys: #{@data}"
104
- end
105
- end
106
-
107
- class Deepstream::Client
108
- def initialize(address, port = 6021, credentials = {})
109
- @address, @port, @unread_msg, @event_callbacks, @records, @max_timeout, @timeout = address, port, nil, {}, {}, 60, 1
110
- connect(credentials)
111
- end
112
-
113
- attr_accessor :verbose, :max_timeout
114
- attr_reader :connected
115
-
116
- def _login(credentials)
117
- _write("A", "REQ", credentials.to_json)
118
- ack = _read_socket
119
- raise unless ack == %w{A A} || (ack == %w{C A} && _read_socket == %w{A A})
120
- end
121
-
122
- def _read_socket(timeout: nil)
123
- Timeout.timeout(timeout) do
124
- @socket.gets(30.chr).tap { |m| break m.chomp(30.chr).split(31.chr) if m }
125
- end
126
- end
127
-
128
- def connect(credentials)
129
- return self if @connected
130
- Thread.start do
131
- Thread.current[:name] = "reader#{object_id}"
132
- loop do
133
- break if @connected # ensures only one thread remains after reconnection
134
- begin
135
- Timeout.timeout(2) { @socket = TCPSocket.new(@address, @port) }
136
- _login(credentials)
137
- @connected = true
138
- print Time.now.to_s[/.+ .+ /], "Connected\n" if @verbose
139
- Thread.start do
140
- _sync_records
141
- _resubscribe_events
142
- end
143
- loop do
144
- @timeout = 1
145
- begin
146
- _process_msg(_read_socket(timeout: 10))
147
- rescue Timeout::Error
148
- _write("heartbeat") # send anything to check if deepstream responds
149
- _process_msg(_read_socket(timeout: 10))
150
- end
151
- end
152
- rescue => e
153
- @connected = false
154
- @socket.close rescue nil
155
- print Time.now.to_s[/.+ .+ /], "Can't connect to deepstream server\n" if @verbose
156
- print "Error: ", e.message, "\n" if @verbose
157
- sleep @timeout
158
- @timeout = [@timeout * 1.2, @max_timeout].min
159
- end
160
- end
161
- end
162
- sleep 0.5
163
- self
164
- end
165
-
166
- def disconnect
167
- @connected = false
168
- @socket.close rescue nil
169
- Thread.list.find { |x| x[:name] == "reader#{object_id}" }.kill
170
- self
171
- end
172
-
173
- def emit(event, value = nil, opts = { timeout: nil })
174
- result = nil
175
- Timeout::timeout(opts[:timeout]) do
176
- sleep 1 until (result = _write('E', 'EVT', event, _typed(value)) rescue false) || opts[:timeout].nil?
177
- end
178
- result
179
- end
180
-
181
- def on(event, &block)
182
- _write_and_read('E', 'S', event)
183
- @event_callbacks[event] = block
184
- rescue => e
185
- print "Error: ", e.message, "\n" if @verbose
186
- @event_callbacks[event] = block
187
- end
188
-
189
- def get(record_name)
190
- get_record(record_name)
191
- end
192
-
193
- def get_record(record_name, list: nil)
194
- name = list ? "#{list}/#{record_name}" : record_name
195
- if list
196
- @records[list] ||= get_list(list)
197
- @records[list].add(name)
198
- end
199
- @records[name] ||= (
200
- _write_and_read('R', 'CR', name)
201
- msg = _read
202
- Deepstream::Record.new(self, name, _parse_data(msg[4]), msg[3].to_i)
203
- )
204
- @records[name]
205
- rescue => e
206
- print "Error: ", e.message, "\n" if @verbose
207
- @records[name] = Deepstream::Record.new(self, name, OpenStruct.new, 0)
208
- end
209
-
210
- def get_list(list_name)
211
- @records[list_name] ||= (
212
- _write_and_read('R', 'CR', list_name)
213
- msg = _read
214
- Deepstream::List.new(self, list_name, _parse_data(msg[4]), msg[3].to_i)
215
- )
216
- rescue => e
217
- print "Error: ", e.message, "\n" if @verbose
218
- @records[list_name] = Deepstream::List.new(self, list_name, [], 0)
219
- end
220
-
221
- def delete(record_name)
222
- if matching = record_name.match(/(?<namespace>\w+)\/(?<record>.+)/)
223
- tmp = get_list(matching[:namespace])
224
- tmp.remove(record_name)
225
- end
226
- _write('R', 'D', record_name)
227
- rescue => e
228
- print "Error: ", e.message, "\n" if @verbose
229
- false
230
- end
231
-
232
- def _resubscribe_events
233
- @event_callbacks.keys.each do |event|
234
- _write_and_read('E', 'S', event)
235
- end
236
- end
237
-
238
- def _sync_records
239
- @records.each do |name, record|
240
- _write_and_read('R', 'CR', name)
241
- msg = _read
242
- @records[name]._update(msg[3].to_i, _parse_data(msg[4]))
243
- end
244
- end
245
-
246
- def _write_and_read(*args)
247
- @unread_msg = nil
248
- _write(*args)
249
- yield _read if block_given?
250
- end
251
-
252
- def _write(*args)
253
- @socket.write(args.join(31.chr) + 30.chr)
254
- rescue => e
255
- raise "not connected" unless @connected
256
- raise e
257
- end
258
-
259
- def _process_msg(msg)
260
- case msg[0..1]
261
- when %w{E EVT} then _fire_event_callback(msg)
262
- when %w{R P} then @records[msg[2]]._patch(msg[3], msg[4], _parse_data(msg[5]))
263
- when %w{R U} then @records[msg[2]]._update(msg[3], _parse_data(msg[4]))
264
- when %w{R A} then @records.delete(msg[3]) if msg[2] == 'D'
265
- when %w{E A} then nil
266
- when %w{X E} then nil
267
- when [] then nil
268
- else
269
- @unread_msg = msg
270
- end
271
- end
272
-
273
- def _read
274
- loop { break @unread_msg || (next sleep 0.01) }.tap { @unread_msg = nil }
275
- end
276
-
277
- def _fire_event_callback(msg)
278
- @event_callbacks[msg[2]].tap { |cb| Thread.start { cb.(_parse_data(msg[3])) } if cb }
279
- end
280
-
281
- def _typed(value)
282
- case value
283
- when Hash then "O#{value.to_json}"
284
- when String then "S#{value}"
285
- when Numeric then "N#{value}"
286
- when TrueClass then 'T'
287
- when FalseClass then 'F'
288
- when NilClass then 'L'
289
- end
290
- end
291
-
292
- def _parse_data(payload)
293
- case payload[0]
294
- when 'O' then JSON.parse(payload[1..-1], object_class: OpenStruct)
295
- when '{' then JSON.parse(payload, object_class: OpenStruct)
296
- when 'S' then payload[1..-1]
297
- when 'N' then payload[1..-1].to_f
298
- when 'T' then true
299
- when 'F' then false
300
- when 'L' then nil
301
- else JSON.parse(payload, object_class: OpenStruct)
302
- end
303
- end
304
-
305
- def inspect
306
- "Deepstream::Client #{@address}:#{@port} connected: #{@connected}"
307
- end
308
- end
14
+ require 'deepstream/client'
@@ -0,0 +1,17 @@
1
+ module Deepstream
2
+ class AckTimeoutRegistry
3
+ def initialize(client)
4
+ @client = client
5
+ @timeouts = {}
6
+ end
7
+
8
+ def add(name, message)
9
+ return unless (timeout = @client.options[:ack_timeout])
10
+ @timeouts[name] = Celluloid.after(timeout) { @client.on_error(message) }
11
+ end
12
+
13
+ def cancel(name)
14
+ @timeouts.delete(name)&.cancel
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,205 @@
1
+ require 'forwardable'
2
+ require 'celluloid/websocket/client'
3
+ require 'deepstream/constants'
4
+ require 'deepstream/error_handler'
5
+ require 'deepstream/event_handler'
6
+ require 'deepstream/record_handler'
7
+ require 'deepstream/helpers'
8
+ require 'deepstream/message'
9
+ require 'deepstream/exceptions'
10
+
11
+ module Deepstream
12
+ class Client
13
+ attr_reader :last_hearbeat, :options, :state
14
+
15
+ include Celluloid
16
+ include Celluloid::Internals::Logger
17
+ extend Forwardable
18
+
19
+ execute_block_on_receiver :on, :subscribe, :listen
20
+
21
+ def_delegators :@event_handler, :on, :emit, :subscribe, :unsubscribe, :listen, :resubscribe, :unlisten
22
+ def_delegators :@error_handler, :error, :on_error
23
+ def_delegators :@record_handler, :get, :set, :delete, :discard, :get_list
24
+
25
+ def initialize(url, options = {})
26
+ @url = Helpers.url(url)
27
+ @error_handler = ErrorHandler.new(self)
28
+ @record_handler = RecordHandler.new(self)
29
+ @event_handler = EventHandler.new(self)
30
+ @options = Helpers.default_options.merge!(options)
31
+ @message_buffer = []
32
+ @last_hearbeat = nil
33
+ @challenge_denied, @login_requested, @deliberate_close = false
34
+ @failed_reconnect_attempts = 0
35
+ @state = CONNECTION_STATE::CLOSED
36
+ Celluloid.logger.level = @options[:verbose] ? LOG_LEVEL::INFO : LOG_LEVEL::OFF
37
+ async.connect
38
+ end
39
+
40
+ def on_open
41
+ @state = CONNECTION_STATE::AWAITING_CONNECTION
42
+ @failed_reconnect_attempts = 0
43
+ end
44
+
45
+ def on_message(data)
46
+ message = Message.new(data)
47
+ info("Incoming message: #{message.inspect}")
48
+ case message.topic
49
+ when TOPIC::AUTH then authentication_message(message)
50
+ when TOPIC::CONNECTION then connection_message(message)
51
+ when TOPIC::EVENT then @event_handler.on_message(message)
52
+ when TOPIC::ERROR then @error_handler.on_error(message)
53
+ when TOPIC::RECORD then @record_handler.on_message(message)
54
+ when TOPIC::RPC then raise UnknownTopic('RPC is currently not implemented.')
55
+ else raise UnknownTopic(message.to_s)
56
+ end
57
+ rescue => e
58
+ @error_handler.on_exception(e)
59
+ end
60
+
61
+ def on_close(code, reason)
62
+ info("Websocket connection closed: #{code.inspect}, #{reason.inspect}")
63
+ @state = CONNECTION_STATE::CLOSED
64
+ reconnect unless @deliberate_close
65
+ rescue => e
66
+ @error_handler.on_exception(e)
67
+ end
68
+
69
+ def login(credentials = @options[:credentials])
70
+ @login_requested = true
71
+ @options[:credentials] = credentials
72
+ if @challenge_denied
73
+ on_error("this client's connection was closed")
74
+ elsif !connected?
75
+ async.connect
76
+ elsif @state == CONNECTION_STATE::AUTHENTICATING
77
+ @login_requested = false
78
+ send_message(TOPIC::AUTH, ACTION::REQUEST, @options[:credentials].to_json)
79
+ end
80
+ self
81
+ rescue => e
82
+ @error_handler.on_exception(e)
83
+ self
84
+ end
85
+
86
+ def close
87
+ return unless connected?
88
+ @deliberate_close = true
89
+ @connection.close
90
+ @connection.terminate
91
+ @state = CONNECTION_STATE::CLOSED
92
+ rescue => e
93
+ @error_handler.on_exception(e)
94
+ end
95
+
96
+ def connected?
97
+ @state != CONNECTION_STATE::CLOSED
98
+ end
99
+
100
+ def logged_in?
101
+ @state == CONNECTION_STATE::OPEN
102
+ end
103
+
104
+ def inspect
105
+ "#{self.class} #{@url} | connection state: #{@state}"
106
+ end
107
+
108
+ def send_message(*args)
109
+ message = Message.parse(*args)
110
+ if !logged_in? && message.needs_authentication?
111
+ info("Placing message #{message.inspect} in buffer, waiting for connection")
112
+ @message_buffer << message
113
+ else
114
+ info("Sending message: #{message.inspect}")
115
+ @connection.text(message.to_s)
116
+ end
117
+ rescue => e
118
+ @error_handler.on_exception(e)
119
+ end
120
+
121
+ private
122
+
123
+ def connection_message(message)
124
+ case message.action
125
+ when ACTION::ACK then on_connection_ack
126
+ when ACTION::CHALLENGE then on_challenge
127
+ when ACTION::ERROR then on_error(message)
128
+ when ACTION::PING then on_ping
129
+ when ACTION::REDIRECT then on_redirection(message)
130
+ when ACTION::REJECTION then on_rejection
131
+ else raise UnknownAction(message)
132
+ end
133
+ end
134
+
135
+ def authentication_message(message)
136
+ case message.action
137
+ when ACTION::ACK then on_login
138
+ when ACTION::ERROR then on_error(message)
139
+ else raise UnknownAction(message)
140
+ end
141
+ end
142
+
143
+ def on_challenge
144
+ @state = CONNECTION_STATE::CHALLENGING
145
+ send_message(TOPIC::CONNECTION, ACTION::CHALLENGE_RESPONSE, @url)
146
+ end
147
+
148
+ def on_connection_ack
149
+ @state = CONNECTION_STATE::AUTHENTICATING
150
+ login if @options[:autologin] || @login_requested
151
+ end
152
+
153
+ def on_ping
154
+ @last_heartbeat = Time.now
155
+ send_message(TOPIC::CONNECTION, ACTION::PONG)
156
+ end
157
+
158
+ def on_login
159
+ @state = CONNECTION_STATE::OPEN
160
+ @message_buffer.each { |message| send_message(message) }.clear
161
+ every(@options[:heartbeat_interval]) { check_heartbeat } if @options[:heartbeat_interval]
162
+ end
163
+
164
+ def on_rejection
165
+ @challenge_denied = true
166
+ close
167
+ end
168
+
169
+ def check_heartbeat
170
+ return unless @last_heartbeat && Time.now - @last_heartbeat > 2 * @options[:heartbeat_interval]
171
+ @state = CONNECTION_STATE::CLOSED
172
+ on_error('Two connections heartbeats missed successively')
173
+ end
174
+
175
+ def on_redirection(message)
176
+ close
177
+ connect(message.data.last)
178
+ end
179
+
180
+ def connect(url = @url, reraise = false)
181
+ @connection = Celluloid::WebSocket::Client.new(url, Actor.current)
182
+ rescue => e
183
+ reraise ? raise : @error_handler.on_exception(e)
184
+ end
185
+
186
+ def reconnect
187
+ @state = CONNECTION_STATE::RECONNECTING
188
+ if @failed_reconnect_attempts < @options[:max_reconnect_attempts]
189
+ connect(@url, true)
190
+ resubscribe
191
+ else
192
+ @state = CONNECTION_STATE::ERROR
193
+ end
194
+ rescue Errno::ECONNREFUSED
195
+ @failed_reconnect_attempts += 1
196
+ on_error("Can't connect! Deepstream server unreachable on #{@url}")
197
+ sleep(reconnect_interval)
198
+ reconnect
199
+ end
200
+
201
+ def reconnect_interval
202
+ [@options[:reconnect_interval] * @failed_reconnect_attempts, @options[:max_reconnect_interval]].min
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,75 @@
1
+ module Deepstream
2
+ MESSAGE_SEPARATOR = 30.chr
3
+ MESSAGE_PART_SEPARATOR = 31.chr
4
+
5
+ module LOG_LEVEL
6
+ DEBUG = 0
7
+ INFO = 1
8
+ WARN = 2
9
+ ERROR = 3
10
+ OFF = 100
11
+ end
12
+
13
+ module CONNECTION_STATE
14
+ CLOSED = :closed
15
+ AWAITING_CONNECTION = :awaiting_connection
16
+ CHALLENGING = :challenging
17
+ AWAITING_AUTHENTICATION = :awaiting_authentication
18
+ AUTHENTICATING = :authenticating
19
+ OPEN = :open
20
+ ERROR = :error
21
+ RECONNECTING = :reconnecting
22
+ end
23
+
24
+ module TOPIC
25
+ CONNECTION = :C
26
+ AUTH = :A
27
+ ERROR = :X
28
+ EVENT = :E
29
+ RECORD = :R
30
+ RPC = :P
31
+ end
32
+
33
+ module ACTION
34
+ ACK = :A
35
+ READ = :R
36
+ REDIRECT = :RED
37
+ CHALLENGE = :CH
38
+ CHALLENGE_RESPONSE = :CHR
39
+ CREATE = :C
40
+ UPDATE = :U
41
+ PATCH = :P
42
+ DELETE = :D
43
+ SUBSCRIBE = :S
44
+ UNSUBSCRIBE = :US
45
+ HAS = :H
46
+ SNAPSHOT = :SN
47
+ LISTEN = :L
48
+ UNLISTEN = :UL
49
+ LISTEN_ACCEPT = :LA
50
+ LISTEN_REJET = :LR
51
+ SUBSCRIPTION_HAS_PROVIDER = :SH
52
+ SUBSCRIPTION_FOR_PATTERN_FOUND = :SP
53
+ SUBSCRIPTION_FOR_PATTERN_REMOVED = :SR
54
+ PROVIDER_UPDATE = :PU
55
+ QUERY = :Q
56
+ CREATEORREAD = :CR
57
+ EVENT = :EVT
58
+ ERROR = :E
59
+ REQUEST = :REQ
60
+ RESPONSE = :RES
61
+ REJECTION = :REJ
62
+ PING = :PI
63
+ PONG = :PO
64
+ end
65
+
66
+ module TYPE
67
+ STRING = :S
68
+ OBJECT = :O
69
+ NUMBER = :N
70
+ NULL = :L
71
+ TRUE = :T
72
+ FALSE = :F
73
+ UNDEFINED = :U
74
+ end
75
+ end
@@ -0,0 +1,30 @@
1
+ require 'deepstream/constants'
2
+ require 'deepstream/helpers'
3
+ require 'deepstream/message'
4
+
5
+ module Deepstream
6
+ class ErrorHandler
7
+ attr_reader :error
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ @error = nil
12
+ end
13
+
14
+ def on_error(message)
15
+ @error =
16
+ if message.is_a?(Message)
17
+ message.topic == TOPIC::ERROR ? message.data : Helpers.to_type(message.data.last)
18
+ else
19
+ message
20
+ end
21
+ puts @error
22
+ end
23
+
24
+ def on_exception(exception)
25
+ raise exception if @client.options[:debug]
26
+ puts exception.message
27
+ puts exception.backtrace
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,75 @@
1
+ require 'deepstream/ack_timeout_registry'
2
+ require 'deepstream/constants'
3
+ require 'deepstream/helpers'
4
+
5
+ module Deepstream
6
+ class EventHandler
7
+ def initialize(client)
8
+ @client = client
9
+ @callbacks = {}
10
+ @listeners = {}
11
+ @ack_timeout_registry = AckTimeoutRegistry.new(@client)
12
+ end
13
+
14
+ def on(event, &block)
15
+ unless @callbacks[event]
16
+ @client.send_message(TOPIC::EVENT, ACTION::SUBSCRIBE, event)
17
+ @ack_timeout_registry.add(event, "No ACK message received in time for #{event}")
18
+ end
19
+ @callbacks[event] = block
20
+ end
21
+ alias subscribe on
22
+
23
+ def listen(pattern, &block)
24
+ pattern = pattern.is_a?(Regexp) ? pattern.source : pattern
25
+ @listeners[pattern] = block
26
+ @client.send_message(TOPIC::EVENT, ACTION::LISTEN, pattern)
27
+ @ack_timeout_registry.add(pattern, "No ACK message received in time for #{pattern}")
28
+ end
29
+
30
+ def unlisten(pattern)
31
+ pattern = pattern.is_a?(Regexp) ? pattern.source : pattern
32
+ @listeners.delete(pattern)
33
+ @client.send_message(TOPIC::EVENT, ACTION::UNLISTEN, pattern)
34
+ end
35
+
36
+ def on_message(message)
37
+ case message.action
38
+ when ACTION::ACK then @ack_timeout_registry.cancel(message.data.last)
39
+ when ACTION::EVENT then fire_event_callback(message)
40
+ when ACTION::SUBSCRIPTION_FOR_PATTERN_FOUND then fire_listen_callback(message)
41
+ when ACTION::SUBSCRIPTION_FOR_PATTERN_REMOVED then fire_listen_callback(message)
42
+ else @client.on_error(message)
43
+ end
44
+ end
45
+
46
+ def emit(event, data = nil)
47
+ @client.send_message(TOPIC::EVENT, ACTION::EVENT, event, Helpers.to_deepstream_type(data))
48
+ end
49
+
50
+ def unsubscribe(event)
51
+ @callbacks.delete(event)
52
+ @client.send_message(TOPIC::EVENT, ACTION::UNSUBSCRIBE, event)
53
+ end
54
+
55
+ def resubscribe
56
+ @callbacks.keys.each { |event| @client.send_message(TOPIC::EVENT, ACTION::SUBSCRIBE, event) }
57
+ @listeners.keys.each { |pattern| @client.send_message(TOPIC::EVENT, ACTION::LISTEN, pattern) }
58
+ end
59
+
60
+ private
61
+
62
+ def fire_event_callback(message)
63
+ event, data = message.data
64
+ data = Helpers.to_type(data)
65
+ Celluloid::Future.new { @callbacks[event].call(event, data) }
66
+ end
67
+
68
+ def fire_listen_callback(message)
69
+ is_subscribed = message.action == ACTION::SUBSCRIPTION_FOR_PATTERN_FOUND
70
+ pattern, event = message.data
71
+ return @client.on_error(pattern) unless @listeners[pattern]
72
+ @listeners[pattern].call(is_subscribed, event)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module Deepstream
2
+ class UnknownTopic < StandardError; end
3
+ class UnknownAction < StandardError; end
4
+ end
@@ -0,0 +1,55 @@
1
+ require 'json'
2
+
3
+ module Deepstream
4
+ module Helpers
5
+ SCHEME = 'ws://'
6
+ DEFAULT_PORT = 6020
7
+ DEFAULT_PATH = 'deepstream'
8
+
9
+ def self.to_deepstream_type(value)
10
+ case value
11
+ when Hash then "O#{value.to_json}"
12
+ when String then "S#{value}"
13
+ when Numeric then "N#{value}"
14
+ when TrueClass then 'T'
15
+ when FalseClass then 'F'
16
+ when NilClass then 'L'
17
+ end
18
+ end
19
+
20
+ def self.to_type(payload)
21
+ case payload[0]
22
+ when 'O' then JSON.parse(payload[1..-1])
23
+ when '{' then JSON.parse(payload)
24
+ when 'S' then payload[1..-1]
25
+ when 'N' then payload[1..-1].to_f
26
+ when 'T' then true
27
+ when 'F' then false
28
+ when 'L' then nil
29
+ else JSON.parse(payload)
30
+ end
31
+ end
32
+
33
+ def self.default_options
34
+ {
35
+ ack_timeout: nil,
36
+ autologin: true,
37
+ credentials: {},
38
+ heartbeat_interval: nil,
39
+ max_reconnect_attempts: 5,
40
+ max_reconnect_interval: 30,
41
+ reconnect_interval: 1,
42
+ verbose: false,
43
+ debug: false
44
+ }
45
+ end
46
+
47
+ def self.url(url)
48
+ url.tap do |url|
49
+ url.prepend(SCHEME) unless url.start_with?(SCHEME)
50
+ url.concat(":#{DEFAULT_PORT}") unless url[/\:\d+/]
51
+ url.concat("/#{DEFAULT_PATH}") unless url[/\/\w+$/]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,31 @@
1
+ require 'deepstream/record'
2
+
3
+ module Deepstream
4
+ class List < Record
5
+ def initialize(*args)
6
+ super
7
+ @data = []
8
+ end
9
+
10
+ def add(record_name)
11
+ set(@data.length.to_s, record_name) unless @data.include?(record_name)
12
+ end
13
+
14
+ def read(version, data)
15
+ @version = version.to_i
16
+ data = JSON.parse(data)
17
+ if data.is_a?(Array)
18
+ @data.concat(data).uniq!
19
+ set(@data) if @data.size > data.size
20
+ end
21
+ end
22
+
23
+ def remove(record_name)
24
+ set(@data) if @data.delete(record_name)
25
+ end
26
+
27
+ def all
28
+ @data.map { |record_name| @client.get(record_name) }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+ require 'deepstream/constants'
3
+
4
+ module Deepstream
5
+ class Message
6
+ attr_reader :topic, :action, :data
7
+
8
+ def self.parse(*args)
9
+ args.first.is_a?(self) ? args.first : new(*args)
10
+ end
11
+
12
+ def initialize(*args)
13
+ if args.one?
14
+ args = args.first.delete(MESSAGE_SEPARATOR).split(MESSAGE_PART_SEPARATOR)
15
+ end
16
+ @topic, @action = args.take(2).map(&:to_sym)
17
+ @data = args.drop(2)
18
+ end
19
+
20
+ def to_s
21
+ args = [@topic, @action]
22
+ args << @data unless @data.empty?
23
+ args.join(MESSAGE_PART_SEPARATOR).concat(MESSAGE_SEPARATOR)
24
+ end
25
+
26
+ def inspect
27
+ "#{self.class.name}: #{@topic} #{@action} #{@data}"
28
+ end
29
+
30
+ def needs_authentication?
31
+ ![TOPIC::CONNECTION, TOPIC::AUTH].include?(@topic)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ require 'json'
2
+ require 'deepstream/constants'
3
+ require 'deepstream/helpers'
4
+
5
+ module Deepstream
6
+ class Record
7
+ attr_reader :name, :data, :version
8
+
9
+ def initialize(client, name)
10
+ @client = client
11
+ @name = name
12
+ @data, @version = nil
13
+ @client.send_message(TOPIC::RECORD, ACTION::CREATEORREAD, @name)
14
+ end
15
+
16
+ def inspect
17
+ "#{self.class} #{@name} #{@version} #{@data}"
18
+ end
19
+
20
+ def unsubscribe
21
+ @client.send_message(TOPIC::RECORD, ACTION::UNSUBSCRIBE, name)
22
+ end
23
+
24
+ def delete
25
+ @client.delete(@name)
26
+ end
27
+
28
+ def set(*args)
29
+ if args.one?
30
+ @data = args.first
31
+ @client.send_message(TOPIC::RECORD, ACTION::UPDATE, @name, (@version += 1), @data.to_json) if @version
32
+ elsif args.size == 2
33
+ path, value = args
34
+ set_path(@data, path, value)
35
+ @client.send_message(TOPIC::RECORD, ACTION::PATCH, @name, (@version += 1), path, Helpers.to_deepstream_type(value)) if @version
36
+ end
37
+ end
38
+
39
+ def read(version, data)
40
+ update(version, data)
41
+ end
42
+
43
+ def patch(version, path, value)
44
+ @version = version.to_i
45
+ set_path(@data, path, Helpers.to_type(value))
46
+ end
47
+
48
+ def update(version, data)
49
+ @version = version.to_i
50
+ @data = JSON.parse(data)
51
+ end
52
+
53
+ private
54
+
55
+ def set_path(data, path, value)
56
+ key, subkey = path.split('.', 2)
57
+ if data.is_a?(Hash)
58
+ subkey ? set_path(data.fetch(key), subkey, value) : data[key] = value
59
+ elsif data.is_a?(Array)
60
+ subkey ? set_path(data[key.to_i], subkey, value) : data[key.to_i] = value
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ require 'deepstream/constants'
2
+ require 'deepstream/record'
3
+ require 'deepstream/list'
4
+
5
+ module Deepstream
6
+ class RecordHandler
7
+ def initialize(client)
8
+ @client = client
9
+ @records = {}
10
+ end
11
+
12
+ def on_message(message)
13
+ case message.action
14
+ when ACTION::ACK then nil
15
+ when ACTION::PATCH then patch(message)
16
+ when ACTION::READ then read(message)
17
+ when ACTION::UPDATE then update(message)
18
+ else @client.on_error(message)
19
+ end
20
+ end
21
+
22
+ def get(name, list: nil)
23
+ if list
24
+ name.prepend("#{list}/")
25
+ @records[list] ||= List.new(@client, list)
26
+ @records[list].add(name)
27
+ end
28
+ @records[name] ||= Record.new(@client, name)
29
+ end
30
+
31
+ def get_list(name)
32
+ @records[name] ||= List.new(@client, name)
33
+ end
34
+
35
+ def set(name, *args)
36
+ @records[name]&.set(*args)
37
+ end
38
+
39
+ def unsubscribe(name)
40
+ @records[name]&.unsubscribe
41
+ end
42
+
43
+ def discard(name)
44
+ unsubscribe(name)
45
+ end
46
+
47
+ def delete(name)
48
+ @client.send_message(TOPIC::RECORD, ACTION::DELETE, name) if @records.delete(name)
49
+ end
50
+
51
+ private
52
+
53
+ def read(message)
54
+ name, *data = message.data
55
+ @records[name]&.read(*data)
56
+ end
57
+
58
+ def update(message)
59
+ name, *data = message.data
60
+ @records[name]&.update(*data)
61
+ end
62
+
63
+ def patch(message)
64
+ name, *data = message.data
65
+ @records[name]&.patch(*data)
66
+ end
67
+ end
68
+ end
metadata CHANGED
@@ -1,15 +1,99 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deepstream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Currency-One S.A.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-09-14 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2017-02-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: celluloid-websocket-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cucumber
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: reel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-expectations
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
13
97
  description: Basic ruby client for the deepstream.io server
14
98
  email:
15
99
  - piotr.szczudlak@currency-one.com
@@ -18,11 +102,24 @@ extensions: []
18
102
  extra_rdoc_files: []
19
103
  files:
20
104
  - ".gitignore"
105
+ - ".gitmodules"
21
106
  - Gemfile
22
107
  - LICENSE
23
108
  - README.md
109
+ - Rakefile
24
110
  - deepstream.gemspec
25
111
  - lib/deepstream.rb
112
+ - lib/deepstream/ack_timeout_registry.rb
113
+ - lib/deepstream/client.rb
114
+ - lib/deepstream/constants.rb
115
+ - lib/deepstream/error_handler.rb
116
+ - lib/deepstream/event_handler.rb
117
+ - lib/deepstream/exceptions.rb
118
+ - lib/deepstream/helpers.rb
119
+ - lib/deepstream/list.rb
120
+ - lib/deepstream/message.rb
121
+ - lib/deepstream/record.rb
122
+ - lib/deepstream/record_handler.rb
26
123
  homepage: https://github.com/Currency-One/deepstream-ruby
27
124
  licenses:
28
125
  - Apache-2.0
@@ -35,7 +132,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
35
132
  requirements:
36
133
  - - ">="
37
134
  - !ruby/object:Gem::Version
38
- version: '0'
135
+ version: 2.3.0
39
136
  required_rubygems_version: !ruby/object:Gem::Requirement
40
137
  requirements:
41
138
  - - ">="
@@ -43,9 +140,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
140
  version: '0'
44
141
  requirements: []
45
142
  rubyforge_project:
46
- rubygems_version: 2.5.1
143
+ rubygems_version: 2.6.8
47
144
  signing_key:
48
145
  specification_version: 4
49
146
  summary: deepstream.io ruby client
50
147
  test_files: []
51
- has_rdoc: