seapig-client-ruby 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 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: []