seapig-client-ruby 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a2ff3c8fd5b96df4a2642b77584d2ac0f6da66ad
4
+ data.tar.gz: 8e9571f50d92b5805304bdaa49606b6906060aac
5
+ SHA512:
6
+ metadata.gz: 8c93cf68cf379b969aa0756b5de9265001ef48a15c141fbdae85e0b0d8d419b61ae73d1dcb1ed5625594d70f5d804ac34f06fc1b9e724290efb02056e99b079a
7
+ data.tar.gz: e391af96eeb2170b8b9c58a8a3fd17f296dc00af4f3f58c9be54493dc5d35025afd063d3815bb57942d0f6814611c26fed3f85fab0e491551384d66ab9355f80
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015-2017 yunta
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Seapig'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+ Bundler::GemHelper.install_tasks
20
+
21
+ require 'rake/testtask'
22
+
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << 'lib'
25
+ t.libs << 'test'
26
+ t.pattern = 'test/**/*_test.rb'
27
+ t.verbose = false
28
+ end
29
+
30
+
31
+ task default: :test
@@ -0,0 +1,27 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'seapig-client'
4
+ require 'slop'
5
+ require 'pp'
6
+
7
+
8
+ OPTIONS = Slop.parse { |o|
9
+ o.string '-c', '--connect', "Seapig server address (default: ws://127.0.0.1:3001)", default: "ws://127.0.0.1:3001"
10
+ o.on '-h', '--help' do puts o; exit end
11
+ }
12
+
13
+ EM.run {
14
+
15
+ SeapigClient.new(OPTIONS["connect"],name: 'observer').slave(OPTIONS.arguments[0]).onchange { |what|
16
+ puts "-"*80 + " " + Time.new.to_s
17
+ puts what.id
18
+ if what.destroyed
19
+ puts "DESTROYED"
20
+ else
21
+ puts what.version
22
+ pp what
23
+ end
24
+ puts
25
+ }
26
+
27
+ }
data/bin/seapig-worker ADDED
@@ -0,0 +1,51 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'seapig-client'
4
+ require 'slop'
5
+
6
+ STDOUT.sync = true
7
+
8
+
9
+ OPTIONS = Slop.parse { |o|
10
+ o.string '-c', '--connect', "Seapig server address (default: ws://127.0.0.1:3001)", default: "ws://127.0.0.1:3001"
11
+ o.on '-h', '--help' do puts o; exit end
12
+ }
13
+
14
+
15
+ class Producer
16
+
17
+ class << self
18
+ attr_reader :patterns
19
+ end
20
+
21
+ def self.all
22
+ @producers ||= ObjectSpace.each_object(Class).select { |klass| klass < Producer }
23
+ end
24
+
25
+ end
26
+
27
+ ($LOAD_PATH+['./lib']).each { |load_path|
28
+ Dir[load_path+'/seapigs/*.rb'].each { |f| require f }
29
+ }
30
+
31
+
32
+
33
+
34
+ EM.run {
35
+
36
+ client = SeapigClient.new(OPTIONS["connect"], name: 'worker')
37
+
38
+ Producer.all.each { |producer|
39
+ producer.patterns.each { |pattern|
40
+ object = client.master(pattern)
41
+ object.onproduce { |child|
42
+ start = Time.new
43
+ print 'Sent %-30s '%[child.id]
44
+ data, version = producer.produce(child.id)
45
+ child.set(object: data, version: version)
46
+ puts 'in %5.2fs - %s'%[(Time.new-start).to_f,version.inspect]
47
+ }
48
+ }
49
+ }
50
+
51
+ }
@@ -0,0 +1,425 @@
1
+ require 'websocket-eventmachine-client'
2
+ require 'json'
3
+ require 'jsondiff'
4
+ require 'hana'
5
+ require 'narray'
6
+
7
+
8
+ module WebSocket
9
+ module Frame
10
+ class Data < String
11
+ def getbytes(start_index, count)
12
+ data = self[start_index, count]
13
+ if @masking_key
14
+ payload_na = NArray.to_na(data,"byte")
15
+ mask_na = NArray.to_na((@masking_key.pack("C*")*((data.size/4) + 1))[0...data.size],"byte")
16
+ data = (mask_na ^ payload_na).to_s
17
+ end
18
+ data
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+
26
+ class SeapigClient
27
+
28
+ attr_reader :uri, :socket, :connected, :error
29
+
30
+ def initialize(uri, options={})
31
+ @uri = uri
32
+ @options = options
33
+ @slave_objects = {}
34
+ @master_objects = {}
35
+ @connected = false
36
+ @socket = nil
37
+ @error = nil
38
+ connect
39
+ end
40
+
41
+
42
+ def connect
43
+
44
+ disconnect if @socket
45
+ @reconnect_on_close = true
46
+
47
+ @timeout_timer ||= EM.add_periodic_timer(10) {
48
+ next if not @socket
49
+ next if Time.new.to_f - @last_communication_at < 20
50
+ puts "Seapig ping timeout, reconnecting" if @options[:debug]
51
+ connect
52
+ }
53
+
54
+ @last_communication_at = Time.new.to_f
55
+
56
+ puts 'Connecting to seapig server' if @options[:debug]
57
+ @socket = WebSocket::EventMachine::Client.connect(uri: @uri)
58
+
59
+ @socket.onopen {
60
+ puts 'Connected to seapig server' if @options[:debug]
61
+ @connected = true
62
+ @error = nil
63
+ @onstatuschange_proc.call(self) if @onstatuschange_proc
64
+ @socket.send JSON.dump(action: 'client-options-set', options: @options)
65
+ @slave_objects.each_pair { |id, object|
66
+ @socket.send JSON.dump(action: 'object-consumer-register', pattern: id, :"version-known" => object.version)
67
+ object.validate
68
+ }
69
+ @master_objects.each_pair { |id, object|
70
+ @socket.send JSON.dump(action: 'object-producer-register', pattern: id, :"version-known" => object.version)
71
+ }
72
+ @last_communication_at = Time.new.to_f
73
+ }
74
+
75
+ @socket.onclose(&(@socket_onclose = Proc.new { |code, reason|
76
+ puts 'Seapig connection closed (code:'+code.inspect+', reason:'+reason.inspect+')' if @options[:debug]
77
+ @connected = false
78
+ @socket = nil
79
+ @timeout_timer.cancel if @timeout_timer
80
+ @timeout_timer = nil
81
+ @slave_objects.values.each { |object| object.invalidate }
82
+ @onstatuschange_proc.call(self) if @onstatuschange_proc
83
+ EM.cancel_timer(@reconnection_timer) if @reconnection_timer
84
+ @reconnection_timer = nil
85
+ @reconnection_timer = EM.add_timer(1) { connect } if @reconnect_on_close
86
+ }))
87
+
88
+ @socket.onerror { |error|
89
+ puts 'Seapig socket error: '+error.inspect if @options[:debug]
90
+ @error = { while: @socket ? "connecting" : "connected", error: error }
91
+ @socket_onclose.call(nil, error) if @socket
92
+ }
93
+
94
+ @socket.onmessage { |data|
95
+ message = JSON.load(data)
96
+ case message['action']
97
+ when 'object-update'
98
+ @slave_objects.each_pair { |id, object|
99
+ object.patch(message) if object.matches(message['id'])
100
+ }
101
+ when 'object-destroy'
102
+ @slave_objects.each_pair { |id, object|
103
+ object.destroy(message) if object.matches(message['id'])
104
+ }
105
+ @master_objects.each_pair { |id, object|
106
+ object.destroy(message) if object.matches(message['id'])
107
+ }
108
+ when 'object-produce'
109
+ handler = @master_objects.values.find { |object| object.matches(message['id']) }
110
+ puts 'Seapig server submitted invalid "produce" request: '+message.inspect if (not handler) and @options[:debug]
111
+ handler.produce(message['id'],message['version-inferred']) if handler
112
+ else
113
+ raise 'Seapig server submitted an unsupported message: '+message.inspect
114
+ end
115
+ @last_communication_at = Time.new.to_f
116
+ }
117
+
118
+ @socket.onping {
119
+ @last_communication_at = Time.new.to_f
120
+ }
121
+
122
+ end
123
+
124
+
125
+ def disconnect(detach_fd = false)
126
+ @reconnect_on_close = false
127
+ if @timeout_timer
128
+ @timeout_timer.cancel
129
+ @timeout_timer = nil
130
+ end
131
+ if @reconnection_timer
132
+ EM.cancel_timer(@reconnection_timer)
133
+ @reconnection_timer = nil
134
+ end
135
+ if @socket
136
+ if detach_fd
137
+ IO.new(@socket.detach).close
138
+ @socket.onclose {}
139
+ @socket_onclose.call("fd detach", "fd detach")
140
+ else
141
+ @socket.onclose {}
142
+ @socket.close
143
+ @socket_onclose.call("close","close")
144
+ end
145
+ end
146
+ end
147
+
148
+
149
+ def detach_fd
150
+ disconnect(true)
151
+ end
152
+
153
+
154
+ def onstatuschange(&block)
155
+ @onstatuschange_proc = block
156
+ self
157
+ end
158
+
159
+
160
+ def slave(id, options={})
161
+ raise "Both or none of 'object' and 'version' are needed" if (options[:object] and not options[:version]) or (not options[:object] and options[:version])
162
+ @slave_objects[id] = if id.include?('*') then SeapigWildcardSlaveObject.new(self, id, options) else SeapigSlaveObject.new(self, id, options) end
163
+ @socket.send JSON.dump(action: 'object-consumer-register', pattern: id, :"version-known" => @slave_objects[id].version) if @connected
164
+ @slave_objects[id]
165
+ end
166
+
167
+
168
+ def master(id, options={})
169
+ @master_objects[id] = if id.include?('*') then SeapigWildcardMasterObject.new(self, id, options) else SeapigMasterObject.new(self, id, options) end
170
+ @socket.send JSON.dump(action: 'object-producer-register', pattern: id, :"version-known" => @master_objects[id].version) if @connected
171
+ @master_objects[id]
172
+ end
173
+
174
+
175
+ def unlink(id)
176
+ if @slave_objects[id]
177
+ @slave_objects.delete(id)
178
+ @socket.send(JSON.stringify(action: 'object-consumer-unregister', pattern: id)) if @connected
179
+ end
180
+ if @master_objects[id]
181
+ @master_objects.delete(id)
182
+ @socket.send(JSON.stringify(action: 'object-producer-unregister', pattern: id)) if @connected
183
+ end
184
+ end
185
+
186
+ end
187
+
188
+
189
+
190
+ class SeapigObject < Hash
191
+
192
+ attr_reader :id, :version, :initialized, :destroyed
193
+
194
+
195
+ def initialize(client, id, options)
196
+ @client = client
197
+ @id = id
198
+ @destroyed = false
199
+ @ondestroy_proc = nil
200
+ @onstatuschange_proc = nil
201
+ @initialized = !!options[:object]
202
+ self.merge!(options[:object]) if options[:object].kind_of?(Hash)
203
+ end
204
+
205
+
206
+ def destroy(id)
207
+ @destroyed = true
208
+ @onstatuschange_proc.call(self) if @onstatuschange_proc
209
+ @ondestroy_proc.call(self) if @ondestroy_proc
210
+ end
211
+
212
+
213
+ def matches(id)
214
+ id =~ Regexp.new(Regexp.escape(@id).gsub('\*','.*?'))
215
+ end
216
+
217
+
218
+ def sanitized
219
+ JSON.load(JSON.dump(self))
220
+ end
221
+
222
+
223
+ def ondestroy(&block)
224
+ @ondestroy_proc = block
225
+ self
226
+ end
227
+
228
+
229
+ def onstatuschange(&block)
230
+ @onstatuschange_proc = block
231
+ self
232
+ end
233
+
234
+
235
+ def unlink
236
+ @client.unlink(@id)
237
+ end
238
+
239
+ end
240
+
241
+
242
+
243
+ class SeapigSlaveObject < SeapigObject
244
+
245
+ attr_reader :received_at, :valid
246
+
247
+
248
+ def initialize(client, id, options)
249
+ super(client, id, options)
250
+ @version = (options[:version] or 0)
251
+ @valid = false
252
+ @received_at = nil
253
+ end
254
+
255
+
256
+ def onchange(&block)
257
+ @onchange_proc = block
258
+ self
259
+ end
260
+
261
+ # ----- for SeapigClient
262
+
263
+ def patch(message)
264
+ @received_at = Time.new
265
+ old_self = JSON.dump(self)
266
+ if (not message['version-old']) or (message['version-old'] == 0) or message.has_key?('value')
267
+ self.clear
268
+ elsif not @version == message['version-old']
269
+ raise "Seapig lost some updates, this should never happen: "+[self, @version, message].inspect
270
+ end
271
+ if message['value']
272
+ self.merge!(message['value'])
273
+ else
274
+ Hana::Patch.new(message['patch']).apply(self)
275
+ end
276
+ @version = message['version-new']
277
+ @valid = true
278
+ @initialized = true
279
+ @onstatuschange_proc.call(self) if @onstatuschange_proc
280
+ @onchange_proc.call(self) if @onchange_proc and old_self != JSON.dump(self)
281
+ end
282
+
283
+
284
+ def validate
285
+ @valid = @initialized
286
+ @onstatuschange_proc.call(self) if @onstatuschange_proc
287
+ end
288
+
289
+
290
+ def invalidate
291
+ @valid = false
292
+ @onstatuschange_proc.call(self) if @onstatuschange_proc
293
+ end
294
+
295
+
296
+ end
297
+
298
+
299
+ class SeapigMasterObject < SeapigObject
300
+
301
+ attr_accessor :stall
302
+
303
+
304
+ def initialize(client, id, options)
305
+ super(client, id, options)
306
+ @version = (options[:version] or [(Time.new.to_f*1000).floor, 0])
307
+ @shadow = self.sanitized
308
+ @stall = false
309
+ end
310
+
311
+
312
+ def onproduce(&block)
313
+ @onproduce_proc = block
314
+ self
315
+ end
316
+
317
+
318
+ def set(options={})
319
+ @version = options[:version] if options[:version]
320
+ if options[:object]
321
+ @stall = false
322
+ self.clear
323
+ self.merge!(options[:object])
324
+ elsif options[:object] == false or options[:stall]
325
+ @stall = true
326
+ end
327
+ @shadow = sanitized
328
+ @initialized = true
329
+ upload(0, {}, @version, @stall ? false : @shadow)
330
+ end
331
+
332
+
333
+ def bump(options={})
334
+ version_old = @version
335
+ data_old = @shadow
336
+ @version = (options[:version] or [version_old[0], version_old[1]+1])
337
+ @shadow = sanitized
338
+ @initialized = true
339
+ upload(version_old, data_old, @version, @stall ? false : @shadow)
340
+ end
341
+
342
+ # ----- for SeapigClient
343
+
344
+ def produce(id, version_inferred)
345
+ if @onproduce_proc
346
+ @onproduce_proc.call(self, version_inferred)
347
+ else
348
+ raise "Master object #{id} has to either be initialized at all times or have an onproduce callback" if not @initialized
349
+ upload(0, {}, @version, @shadow)
350
+ end
351
+ end
352
+
353
+ private
354
+
355
+ def upload(version_old, data_old, version_new, data_new)
356
+ if @client.connected
357
+ if version_old == 0 or data_new == false
358
+ @client.socket.send JSON.dump(id: @id, action: 'object-patch', :"version-new" => version_new, value: data_new)
359
+ else
360
+ diff = JsonDiff.generate(data_old, data_new)
361
+ if JSON.dump(diff).size < JSON.dump(data_new).size #can we afford this?
362
+ @client.socket.send JSON.dump(id: @id, action: 'object-patch', :'version-old' => version_old, :'version-new' => version_new, patch: diff)
363
+ else
364
+ @client.socket.send JSON.dump(id: @id, action: 'object-patch', :'version-new' => version_new, value: data_new)
365
+ end
366
+ end
367
+ end
368
+ self
369
+ end
370
+
371
+ end
372
+
373
+
374
+
375
+
376
+ class SeapigWildcardSlaveObject < SeapigSlaveObject
377
+
378
+
379
+ def patch(message)
380
+ self[message['id']] ||= SeapigSlaveObject.new(@client, message['id'],{}).onchange(&@onchange_proc)
381
+ self[message['id']].patch(message)
382
+ end
383
+
384
+
385
+ def destroy(id)
386
+ return if not destroyed = self.delete(id)
387
+ destroyed.destroy(id)
388
+ end
389
+
390
+ end
391
+
392
+
393
+
394
+ class SeapigWildcardMasterObject < SeapigMasterObject
395
+
396
+ def initialize(client, id, options)
397
+ super(client, id, options)
398
+ @children = ObjectSpace::WeakMap.new
399
+ @options = options
400
+ end
401
+
402
+
403
+ def [](id)
404
+ key = @children.keys.find { |key| key == id }
405
+ @children[(key or id)] ||= SeapigMasterObject.new(@client, id, @options)
406
+ end
407
+
408
+
409
+ def produce(id, version_inferred)
410
+ child = self[id]
411
+ if @onproduce_proc
412
+ @onproduce_proc.call(child, version_inferred)
413
+ else
414
+ child.send
415
+ end
416
+ end
417
+
418
+
419
+ def destroy(message)
420
+ key = @children.keys.find { |key| key == id }
421
+ return if not (key and destroyed = @children[key])
422
+ destroyed.destroy(id)
423
+ end
424
+
425
+ end
@@ -0,0 +1,3 @@
1
+ module Seapig
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require "seapig-client-ruby/client.rb"
2
+
3
+ module SeapigClientRuby
4
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seapig-client-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - yunta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: websocket-eventmachine-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: jsondiff
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
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: hana
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
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: narray
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
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: slop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: meh
84
+ email:
85
+ - maciej.blomberg@mikoton.com
86
+ executables:
87
+ - seapig-observer
88
+ - seapig-worker
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - MIT-LICENSE
93
+ - Rakefile
94
+ - bin/seapig-observer
95
+ - bin/seapig-worker
96
+ - lib/seapig-client-ruby/client.rb
97
+ - lib/seapig-client-ruby/version.rb
98
+ - lib/seapig-client.rb
99
+ homepage: https://github.com/yunta-mb/seapig-client-ruby
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.5.2
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Transient object synchronization lib - client
123
+ test_files: []