goat 0.2.12 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +45 -0
- data/bin/channel-srv +150 -0
- data/bin/press +29 -0
- data/bin/state-srv +390 -0
- data/bin/sync +53 -0
- data/bin/yodel +1 -1
- data/goat.gemspec +37 -0
- data/lib/goat.rb +774 -618
- data/lib/goat/common.rb +53 -0
- data/lib/goat/dynamic.rb +91 -0
- data/lib/goat/extn.rb +94 -5
- data/lib/goat/goat.js +348 -119
- data/lib/goat/html.rb +221 -103
- data/lib/goat/js/component.js +18 -15
- data/lib/goat/net-common.rb +38 -0
- data/lib/goat/notifications.rb +51 -46
- data/lib/goat/state-srv.rb +119 -0
- data/lib/goat/yodel.rb +3 -2
- data/lib/views/plain_layout.erb +7 -3
- metadata +18 -10
- data/lib/goat/logger.rb +0 -39
- data/lib/goat/sinatra.rb +0 -11
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
Goat
|
2
|
+
===
|
3
|
+
|
4
|
+
<http://goatweb.org>
|
5
|
+
|
6
|
+
Description
|
7
|
+
-----------
|
8
|
+
|
9
|
+
Goat makes it easier to build complex, data-heavy websites. It's written in Ruby, and aims to be a good tool for building applications in the class of, say, Facebook: data-heavy, interactive, and real-time.
|
10
|
+
|
11
|
+
Web development presents a fairly subtle set of problems:
|
12
|
+
|
13
|
+
- Distributing application logic between the client and the server. (You usually can't put it all on the client, but it's unpleasant to wait twenty seconds for a page to refresh before you find out the value of a form field is invalid.)
|
14
|
+
- Figuring out how to make changes without interruption -- live data migrations, and code upgrades that (ideally) don't interrupt users.
|
15
|
+
- Varying browsers.
|
16
|
+
- Multiple programming languages (CSS, JavaScript, HTML, server-side languages).
|
17
|
+
- Multiple input devices (touch, mouse, keyboard).
|
18
|
+
- May have to scale massively at short notice
|
19
|
+
|
20
|
+
Goat
|
21
|
+
----
|
22
|
+
|
23
|
+
Goat is page-oriented. Pages are the fundamental building block of the web. Complex data often naturally breaks down into discrete pages. In the URL, pages come with one of the most universally used and understood collaboration tools.
|
24
|
+
|
25
|
+
Figuring out how to combine presenting a significant amount of data presentation with complex user interaction is tough. It's not easy to imagine what Facebook, Wikipedia, Twitter, Gmail or Quora would look like as desktop applications -- and it's unlikely that they'd be better.
|
26
|
+
|
27
|
+
Some applications, such as spreadsheets, probably aren't a good fit for a page-based model, and these applications are not likely to be a good fit for Goat.
|
28
|
+
|
29
|
+
Goat avoids HTML templates, and replaces them with programmatic HTML generation and components: a page is composed of one or more components. Components may persist on the server, if you'd like to be able to update the client-side state of a component in real time. Components also group together the code that may be required for a single UI element: client-side JavaScript, server-side Ruby, and CSS.
|
30
|
+
|
31
|
+
Goat tries to:
|
32
|
+
|
33
|
+
- Make it easy to build pages that don't require refreshing (i.e., live updating of data).
|
34
|
+
- Optimize for the development cycle (all things being equal, the application that's easier to change will end up better).
|
35
|
+
- Make writing client-side JavaScript easier. (Intelligent scoping, easy RPC.)
|
36
|
+
- Make programmatic HTML generation easy.
|
37
|
+
- Not be all-encompassing, but to make sensible assumptions about most parts of the web application stack.
|
38
|
+
|
39
|
+
Design decisions
|
40
|
+
----------------
|
41
|
+
|
42
|
+
- Goat should store JS for some set of components in an external file so that JS can be cached across pages.
|
43
|
+
- You can't have to load 2mb of JS and have a loading bar when a user visits the site for the first time. URLs should look decent.
|
44
|
+
- Make site work well for search engines.
|
45
|
+
- Goat doesn't try to abstract away the fact that the web is built with CSS/HTML/JavaScript.
|
data/bin/channel-srv
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'json'
|
6
|
+
require 'thin'
|
7
|
+
require 'rack'
|
8
|
+
|
9
|
+
%w{extn net-common state-srv}.each {|f| require File.join(File.dirname(__FILE__), "../lib/goat/#{f}")}
|
10
|
+
|
11
|
+
$verbose = ARGV.include?('-v')
|
12
|
+
|
13
|
+
$chsrv_instance = 'chsrv_' + String.random(10)
|
14
|
+
|
15
|
+
module Goat
|
16
|
+
module ChannelSrv
|
17
|
+
class ChannelStateConnection < StateSrvConnection
|
18
|
+
def connection_completed
|
19
|
+
$stderr.puts :connection_completed
|
20
|
+
super
|
21
|
+
|
22
|
+
send_message('register_chsrv', 'chsrv' => $chsrv_instance)
|
23
|
+
end
|
24
|
+
|
25
|
+
def message_received(msg)
|
26
|
+
pusher = ChannelPusher.pusher_for(msg['pgid'])
|
27
|
+
if pusher
|
28
|
+
pusher.statesrv_message_received(msg)
|
29
|
+
else
|
30
|
+
logw "Page #{msg['pgid'].inspect} doesn't appear to be connected to this chsrv"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class ChannelPusher
|
36
|
+
include EM::Deferrable
|
37
|
+
|
38
|
+
@pushers = {}
|
39
|
+
class << self
|
40
|
+
attr_reader :pushers
|
41
|
+
|
42
|
+
def pusher_for(pgid)
|
43
|
+
@pushers[pgid]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :pgid
|
48
|
+
|
49
|
+
def initialize(pgid, jsonp, version)
|
50
|
+
@pgid = pgid
|
51
|
+
@jsonp = jsonp
|
52
|
+
@version = version
|
53
|
+
@messages = []
|
54
|
+
|
55
|
+
self.class.pushers[pgid] = self
|
56
|
+
|
57
|
+
self.errback { cleanup }
|
58
|
+
self.callback { cleanup }
|
59
|
+
|
60
|
+
ChannelStateConnection.send_message('page_connected',
|
61
|
+
'pgid' => pgid,
|
62
|
+
'chsrv' => $chsrv_instance,
|
63
|
+
'version' => @version
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def statesrv_message_received(msg)
|
68
|
+
#TODO hack below for batched messages
|
69
|
+
Log.log_message(msg)
|
70
|
+
if msg['type'] == 'page_updated'
|
71
|
+
@messages += msg['messages']
|
72
|
+
# send on next tick so we get a batch of messages together, rather than a single message
|
73
|
+
EM.next_tick { self.send_and_finish }
|
74
|
+
elsif msg['type'] == 'page_expired'
|
75
|
+
@messages << msg
|
76
|
+
EM.next_tick { self.send_and_finish }
|
77
|
+
else
|
78
|
+
raise "Don't understand this message: #{msg.inspect}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def cleanup
|
83
|
+
logw "connection for #{@pgid}/#{@jsonp} closed"
|
84
|
+
@finished = true
|
85
|
+
self.class.pushers.delete(pgid)
|
86
|
+
@channel.unsubscribe(@sub) if @sub
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_and_finish
|
90
|
+
return if @finished
|
91
|
+
@body_callback.call("#{@jsonp}(#{{'messages' => @messages}.to_json})")
|
92
|
+
succeed
|
93
|
+
end
|
94
|
+
|
95
|
+
def each(&blk)
|
96
|
+
@body_callback = blk
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
module Srv
|
101
|
+
def self.request_failed; [500, {}, 'request failed']; end
|
102
|
+
|
103
|
+
def self.handle_channel(req)
|
104
|
+
pgid = req['_id']
|
105
|
+
jsonp = req['jsonp']
|
106
|
+
ver = req['version']
|
107
|
+
|
108
|
+
return request_failed unless pgid && jsonp && ver
|
109
|
+
|
110
|
+
body = ChannelPusher.new(pgid, jsonp, ver)
|
111
|
+
|
112
|
+
EM.next_tick do
|
113
|
+
req.env['async.callback'].call([200, {
|
114
|
+
'Connection' => 'Keep-Alive',
|
115
|
+
'Content-Type' => 'application/javascript'
|
116
|
+
}, body])
|
117
|
+
end
|
118
|
+
|
119
|
+
[-1, {}, []]
|
120
|
+
|
121
|
+
rescue NoStateSrvConnectionError
|
122
|
+
$stderr.puts "Couldn't register with StateSrv to service a /channel request"
|
123
|
+
request_failed
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.call(env)
|
127
|
+
req = Rack::Request.new(env)
|
128
|
+
|
129
|
+
if req.path == '/channel'
|
130
|
+
handle_channel(req)
|
131
|
+
else
|
132
|
+
[400, {}, 'bad request']
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
if $0 == __FILE__
|
140
|
+
app = Rack::Builder.app do |builder|
|
141
|
+
use Rack::CommonLogger
|
142
|
+
use Rack::ShowExceptions
|
143
|
+
run Goat::ChannelSrv::Srv
|
144
|
+
end
|
145
|
+
|
146
|
+
EM.run do
|
147
|
+
Goat::ChannelSrv::ChannelStateConnection.connect
|
148
|
+
Rack::Handler::Thin.run(app, :Port => (ARGV.first ? ARGV.first.to_i : 8050))
|
149
|
+
end
|
150
|
+
end
|
data/bin/press
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'rubygems'
|
5
|
+
require File.join(File.dirname(__FILE__), '../lib/goat')
|
6
|
+
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.on('-fOPTIONAL', String) {|f| $file = f}
|
9
|
+
opts.on('-cOPTIONAL', String) {|c| $classes = c.split(',')}
|
10
|
+
opts.on('-pOPTIONAL', String) {|p| $classes = File.read(p).strip.split(',')}
|
11
|
+
opts.on('-oOPTIONAL', String) {|w| $specfile = w }
|
12
|
+
opts.on('-j') { $emitjs = true}
|
13
|
+
opts.on('-s') { $emitstyle = true}
|
14
|
+
opts.on('-v') {} # suppress optparse wanting to use -v as version
|
15
|
+
opts.on('-h') {usage; exit 1}
|
16
|
+
end.parse!(ARGV)
|
17
|
+
|
18
|
+
require $file
|
19
|
+
$classes.each do |c|
|
20
|
+
cls = Kernel.fetch_class(c)
|
21
|
+
puts cls.__script if $emitjs && cls.__script
|
22
|
+
puts cls.scoped_css if $emitstyle && cls.__css
|
23
|
+
end
|
24
|
+
|
25
|
+
if $specfile
|
26
|
+
File.open($specfile, 'w') do |f|
|
27
|
+
f.write({:classes => $classes, :created => Time.now.to_i, :created_human => Time.now.to_s}.to_yaml)
|
28
|
+
end
|
29
|
+
end
|
data/bin/state-srv
ADDED
@@ -0,0 +1,390 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'json'
|
6
|
+
require 'set'
|
7
|
+
require 'term/ansicolor'
|
8
|
+
|
9
|
+
$verbose = ARGV.include?('-v')
|
10
|
+
|
11
|
+
$:.unshift(File.join(File.dirname(__FILE__), '../lib'))
|
12
|
+
|
13
|
+
require 'goat/common'
|
14
|
+
require 'goat/net-common'
|
15
|
+
|
16
|
+
module Goat
|
17
|
+
module StateSrv
|
18
|
+
NotifChannel = EM::Channel.new
|
19
|
+
|
20
|
+
class PageDeleted < RuntimeError; end
|
21
|
+
|
22
|
+
# written this clunky way because it'll be manually-memory-managed c++ some day soon
|
23
|
+
class Registry
|
24
|
+
def initialize
|
25
|
+
@components_by_class = {}
|
26
|
+
@components_by_id = {}
|
27
|
+
@components_by_page = {}
|
28
|
+
@last_connected = {}
|
29
|
+
@page_connections = {}
|
30
|
+
@channelsrvs = {}
|
31
|
+
@page_versions = {}
|
32
|
+
@page_updates = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_component(pgid, skel)
|
36
|
+
self.find_class(skel.cls) << skel.id
|
37
|
+
self.find_page(pgid) << skel.id
|
38
|
+
@components_by_id[skel.id] = skel
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_class(cls)
|
42
|
+
@components_by_class[cls] ||= Set.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def components_by_class(cls)
|
46
|
+
find_class(cls).map{|cid| find_id(cid)}
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_id(cid)
|
50
|
+
@components_by_id[cid]
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize_page(pgid)
|
54
|
+
@page_versions[pgid] = 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def incr_page_version(pgid)
|
58
|
+
@page_versions[pgid] += 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def page_version(pgid)
|
62
|
+
@page_versions[pgid]
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_page(pgid)
|
66
|
+
@components_by_page[pgid] ||= Set.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def page_connected(pgid, chsrv, ver)
|
70
|
+
if @page_versions[pgid]
|
71
|
+
@last_connected[pgid] = Time.now
|
72
|
+
@page_connections[pgid] = {:chsrv => chsrv, :version => ver}
|
73
|
+
else
|
74
|
+
logw "Page #{pgid} has been deleted"
|
75
|
+
raise PageDeleted
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def page_disconnected(pgid)
|
80
|
+
@page_connections.delete(pgid)
|
81
|
+
end
|
82
|
+
|
83
|
+
def last_connected_delta(pgid)
|
84
|
+
if last = @last_connected[pgid]
|
85
|
+
Time.now - last
|
86
|
+
else
|
87
|
+
Time.now - Time.at(0)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def connected_pages
|
92
|
+
@page_connections.keys
|
93
|
+
end
|
94
|
+
|
95
|
+
def connected?(pgid)
|
96
|
+
@page_connections[pgid]
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_component(cid)
|
100
|
+
c = @components_by_id[cid]
|
101
|
+
if c
|
102
|
+
if cs = @components_by_class[c.cls]
|
103
|
+
cs.delete(cid)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
@components_by_id.delete(cid)
|
107
|
+
end
|
108
|
+
|
109
|
+
def delete_page(pgid)
|
110
|
+
cs = @components_by_page[pgid]
|
111
|
+
if cs
|
112
|
+
cs.each {|c| delete_component(c)}
|
113
|
+
else
|
114
|
+
logw "(No components for #{pgid})"
|
115
|
+
end
|
116
|
+
|
117
|
+
@page_updates.delete(pgid)
|
118
|
+
@last_connected.delete(pgid)
|
119
|
+
@page_connections.delete(pgid)
|
120
|
+
@page_versions.delete(pgid)
|
121
|
+
@components_by_page.delete(pgid)
|
122
|
+
end
|
123
|
+
|
124
|
+
def register_chsrv(id, conn)
|
125
|
+
@channelsrvs[id] = conn
|
126
|
+
end
|
127
|
+
|
128
|
+
def unregister_chsrv(id, conn)
|
129
|
+
@channelsrvs.delete(id)
|
130
|
+
end
|
131
|
+
|
132
|
+
def chsrv_connection(id)
|
133
|
+
@channelsrvs[id]
|
134
|
+
end
|
135
|
+
|
136
|
+
def page_connection(id)
|
137
|
+
@page_connections[id]
|
138
|
+
end
|
139
|
+
|
140
|
+
def page_updates(pgid)
|
141
|
+
@page_updates[pgid] ||= []
|
142
|
+
end
|
143
|
+
|
144
|
+
def page_updates_for_txn(pgid, txn)
|
145
|
+
if txn
|
146
|
+
if ups = page_updates(pgid).detect{|u| u[:txn] == txn}
|
147
|
+
ups
|
148
|
+
else
|
149
|
+
txnu = {:txn => txn, :ups => []}
|
150
|
+
page_updates(pgid) << txnu
|
151
|
+
txnu
|
152
|
+
end
|
153
|
+
else
|
154
|
+
txnu = {:txn => nil, :ups => []}
|
155
|
+
page_updates(pgid) << txnu
|
156
|
+
txnu
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_update(pgid, txn, up)
|
161
|
+
page_updates_for_txn(pgid, txn)[:ups] << up
|
162
|
+
end
|
163
|
+
|
164
|
+
def memusage
|
165
|
+
$stderr.puts "Components in memory: #{@components_by_id.values.count}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Delegate
|
170
|
+
@registry = Registry.new
|
171
|
+
|
172
|
+
def self.registry; @registry; end
|
173
|
+
|
174
|
+
class << self
|
175
|
+
def ensure_keys(hash, ks)
|
176
|
+
ks.each do |k|
|
177
|
+
unless hash.include?(k)
|
178
|
+
raise IndexError.new("Hash omits key #{k.inspect}")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def register_page(msg)
|
184
|
+
ensure_keys(msg, %w{pgid components})
|
185
|
+
|
186
|
+
pgid = msg['pgid']
|
187
|
+
|
188
|
+
msg['components'].each do |c|
|
189
|
+
@registry.add_component(pgid, ComponentSkeleton.from_hash(c))
|
190
|
+
end
|
191
|
+
|
192
|
+
logd("registered #{msg['components'].count} components")
|
193
|
+
|
194
|
+
@registry.initialize_page(pgid)
|
195
|
+
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
|
199
|
+
def live_components(msg)
|
200
|
+
ensure_keys(msg, %w{class spec})
|
201
|
+
|
202
|
+
@registry.components_by_class(msg['class']).\
|
203
|
+
select{|c| c.spec == msg['spec']}.\
|
204
|
+
select{|c| @registry.connected?(c.pgid) || @registry.last_connected_delta(c.pgid) < 120}.\
|
205
|
+
map(&:to_hash)
|
206
|
+
end
|
207
|
+
|
208
|
+
def fetch_component(msg)
|
209
|
+
ensure_keys(msg, %w{id})
|
210
|
+
|
211
|
+
@registry.find_id(msg['id']).to_hash
|
212
|
+
end
|
213
|
+
|
214
|
+
def send_message(chsrv_id, type, msg)
|
215
|
+
chsrv = @registry.chsrv_connection(chsrv_id)
|
216
|
+
|
217
|
+
if chsrv
|
218
|
+
chsrv.send_message(type, msg)
|
219
|
+
else
|
220
|
+
logw "send_message fail: #{chsrv_id} no longer connected"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def send_updates(pgid)
|
225
|
+
if c = @registry.page_connection(pgid)
|
226
|
+
cur = c[:version]
|
227
|
+
chsrv_id = c[:chsrv]
|
228
|
+
txn_ups = @registry.page_updates(pgid)
|
229
|
+
txn_ups = txn_ups.size > 5 ? txn_ups[-5..-1] : txn_ups
|
230
|
+
ups = txn_ups.map{|x| x[:ups]}.flatten
|
231
|
+
vers = {}
|
232
|
+
txn_ups.each {|txn_up| txn_up[:ups].each{|up| vers[up.version] = txn_up[:txn]}}
|
233
|
+
|
234
|
+
need = ups.select{|u| u.version > cur}
|
235
|
+
|
236
|
+
msgs = []
|
237
|
+
|
238
|
+
need.each do |u|
|
239
|
+
msgs << {
|
240
|
+
'pgid' => pgid,
|
241
|
+
'class' => u.skel.cls,
|
242
|
+
'id' => u.skel.id,
|
243
|
+
'type' => 'component_updated',
|
244
|
+
'updates' => u.mutations,
|
245
|
+
'version' => u.version
|
246
|
+
}
|
247
|
+
|
248
|
+
txn = vers[u.version]
|
249
|
+
msgs << {'type' => 'txn_complete', 'txn' => txn} if txn
|
250
|
+
end
|
251
|
+
|
252
|
+
send_message(chsrv_id, 'page_updated', {'pgid' => pgid, 'messages' => msgs}) unless msgs.empty?
|
253
|
+
else
|
254
|
+
logw "No chsrv connection for page #{pgid}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def send_page_deleted_error(pgid, chsrv_id)
|
259
|
+
send_message(chsrv_id, 'page_expired', {'pgid' => pgid})
|
260
|
+
end
|
261
|
+
|
262
|
+
def handle_component_update(txn, update)
|
263
|
+
comp = @registry.find_id(update.skel.id)
|
264
|
+
|
265
|
+
if comp
|
266
|
+
comp.dom = update.skel.dom
|
267
|
+
comp.state = update.skel.state
|
268
|
+
|
269
|
+
@registry.incr_page_version(comp.pgid)
|
270
|
+
|
271
|
+
update.version = @registry.page_version(comp.pgid)
|
272
|
+
|
273
|
+
@registry.add_update(comp.pgid, txn, update)
|
274
|
+
else
|
275
|
+
$stderr.puts "Couldn't find component #{id}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def components_updated(msg)
|
280
|
+
ensure_keys(msg, %w{txn pgid updates})
|
281
|
+
|
282
|
+
txn = msg['txn']
|
283
|
+
msg['updates'].each{|u| handle_component_update(txn, ComponentUpdate.from_hash(u))}
|
284
|
+
send_updates(msg['pgid'])
|
285
|
+
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
|
289
|
+
def page_connected(msg)
|
290
|
+
ensure_keys(msg, %w{pgid chsrv version})
|
291
|
+
|
292
|
+
pgid = msg['pgid']
|
293
|
+
|
294
|
+
@registry.page_connected(pgid, msg['chsrv'], msg['version'].to_i)
|
295
|
+
send_updates(pgid)
|
296
|
+
nil
|
297
|
+
rescue PageDeleted
|
298
|
+
send_page_deleted_error(pgid, msg['chsrv'])
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
|
302
|
+
def register_chsrv(conn, msg)
|
303
|
+
ensure_keys(msg, %w{chsrv})
|
304
|
+
|
305
|
+
@registry.register_chsrv(msg['chsrv'], conn)
|
306
|
+
nil
|
307
|
+
end
|
308
|
+
|
309
|
+
def unregister_chsrv(conn, msg)
|
310
|
+
@registry.unregister_chsrv(msg['chsrv'], conn)
|
311
|
+
nil
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
class Sweeper
|
317
|
+
def self.start
|
318
|
+
sweeper = self.new
|
319
|
+
|
320
|
+
EM::PeriodicTimer.new(5) do
|
321
|
+
sweeper.run
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def initialize
|
326
|
+
@registry = Delegate.registry
|
327
|
+
end
|
328
|
+
|
329
|
+
def run
|
330
|
+
@registry.connected_pages.each do |pgid|
|
331
|
+
if @registry.last_connected_delta(pgid) > 300
|
332
|
+
logd "Deleting page #{pgid}"
|
333
|
+
@registry.delete_page(pgid)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class RecvServer < EM::Connection
|
340
|
+
include EM::P::LineText2
|
341
|
+
include Goat::JSONMessages
|
342
|
+
|
343
|
+
def self.start(host='127.0.0.1', port=8011)
|
344
|
+
EM.start_server(host, port, self)
|
345
|
+
end
|
346
|
+
|
347
|
+
def receive_line(line)
|
348
|
+
msg = JSON.load(line)
|
349
|
+
Log.log_message(msg) if $verbose
|
350
|
+
|
351
|
+
type = msg['type']
|
352
|
+
if type == 'register_chsrv'
|
353
|
+
Delegate.register_chsrv(self, msg)
|
354
|
+
@chsrv = msg
|
355
|
+
logw "#{@chsrv['chsrv']} connected"
|
356
|
+
elsif Delegate.respond_to?(type)
|
357
|
+
if resp = Delegate.send(type, msg)
|
358
|
+
send_message('response', 'response' => resp)
|
359
|
+
end
|
360
|
+
else
|
361
|
+
$stderr.puts "Delegate doesn't respond to #{type.inspect}"
|
362
|
+
end
|
363
|
+
|
364
|
+
maybe_print_statistics
|
365
|
+
end
|
366
|
+
|
367
|
+
def maybe_print_statistics
|
368
|
+
@last_printed ||= Time.now
|
369
|
+
if Time.now - @last_printed > 10
|
370
|
+
Delegate.registry.memusage
|
371
|
+
end
|
372
|
+
@last_printed = Time.now
|
373
|
+
end
|
374
|
+
|
375
|
+
def unbind
|
376
|
+
if @chsrv
|
377
|
+
logw "#{@chsrv['chsrv']} disconnected"
|
378
|
+
Delegate.unregister_chsrv(self, @chsrv)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
if $0 == __FILE__
|
386
|
+
EM.run do
|
387
|
+
Goat::StateSrv::Sweeper.start
|
388
|
+
Goat::StateSrv::RecvServer.start
|
389
|
+
end
|
390
|
+
end
|