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 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