deepstream 0.1.7 → 0.2.0

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