goat 0.2.12 → 0.3.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.
data/bin/sync ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Goat
4
+ module Sync
5
+ NotifChannel = EM::Channel.new
6
+
7
+ @viewports = {}
8
+
9
+ class Viewport
10
+ attr_reader :created, :viewport, :components
11
+
12
+ def initialize(v, cs)
13
+ @created = Time.now
14
+ @viewport = v
15
+ @components = cs
16
+ end
17
+ end
18
+
19
+ class ComponentServer < EM::Connection
20
+ def self.start(host='127.0.0.1', port=8010)
21
+ EM.start_server(host, port, self)
22
+ end
23
+
24
+ def viewport_components(v, cs)
25
+ @viewports[v] = Viewport.new(v, cs)
26
+ end
27
+
28
+ def receive_line(line)
29
+ msg = JSON.load(line)
30
+ meth = msg['type']
31
+ args = msg['data']
32
+ self.send(meth.to_sym, *args)
33
+ end
34
+ end
35
+
36
+ NotificationServer.subscribe() do |notif|
37
+
38
+ end
39
+
40
+ class RecvServer < EM::Connection
41
+ include EM::P::LineText2
42
+
43
+ def self.start(host='127.0.0.1', port=8001)
44
+ EM.start_server(host, port, self)
45
+ end
46
+
47
+ def receive_line(line)
48
+ $stdout.puts "got #{line}" if $verbose
49
+ NotifChannel << line
50
+ end
51
+ end
52
+ end
53
+ end
data/bin/yodel CHANGED
@@ -9,7 +9,7 @@ $host = '127.0.0.1'
9
9
 
10
10
  def usage
11
11
  $stderr.puts <<-EOH
12
- #{__FILE__} [-b broadcast-port] [-r receive-port] [-h host]
12
+ #{__FILE__} [-b broadcast-port] [-r receive-port] [-H host]
13
13
  EOH
14
14
  end
15
15
 
data/goat.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'goat'
3
+ s.version = '0.3.0'
4
+ s.summary = 'Pre-release beta version of Goat'
5
+ s.author = 'Patrick Collison'
6
+ s.email = 'patrick@collison.ie'
7
+ s.homepage = 'http://goatweb.org'
8
+ s.description = 'Sinatra live'
9
+ s.rubyforge_project = 'goat'
10
+ s.executables = %w{yodel}
11
+ s.require_paths = %w{lib}
12
+
13
+ s.files = %w{
14
+ README.md
15
+ bin/channel-srv
16
+ bin/press
17
+ bin/state-srv
18
+ bin/sync
19
+ bin/yodel
20
+ goat.gemspec
21
+ lib/goat/autobind.rb
22
+ lib/goat/common.rb
23
+ lib/goat/dynamic.rb
24
+ lib/goat/extn.rb
25
+ lib/goat/goat.js
26
+ lib/goat/html.rb
27
+ lib/goat/js/component.js
28
+ lib/goat/mongo.rb
29
+ lib/goat/net-common.rb
30
+ lib/goat/notifications.rb
31
+ lib/goat/state-srv.rb
32
+ lib/goat/static.rb
33
+ lib/goat/yodel.rb
34
+ lib/goat.rb
35
+ lib/views/plain_layout.erb
36
+ }
37
+ end
data/lib/goat.rb CHANGED
@@ -1,3 +1,12 @@
1
+ #
2
+ # Author: Patrick Collison <patrick@collison.ie>
3
+ # Date: Spring-Fall 2010
4
+ #
5
+ # See http://goatweb.org
6
+ #
7
+ # License: GNU General Public License v2
8
+
9
+ require 'rubygems'
1
10
  require 'set'
2
11
  require 'cgi'
3
12
  require 'json'
@@ -5,162 +14,99 @@ require 'eventmachine'
5
14
  require 'thin'
6
15
  require 'term/ansicolor'
7
16
  require 'tilt'
17
+ require 'diff/lcs'
18
+ require 'inline'
8
19
 
9
- %w{logger notifications extn html static autobind}.each do |file|
10
- require File.join(File.dirname(__FILE__), 'goat', file)
11
- end
20
+ $:.unshift(File.dirname(__FILE__))
12
21
 
13
- $verbose ||= ARGV.include?('-v')
22
+ require 'goat/common'
23
+ require 'goat/net-common'
24
+ require 'goat/notifications'
25
+ require 'goat/extn'
26
+ require 'goat/html'
27
+ require 'goat/static'
28
+ require 'goat/autobind'
29
+ require 'goat/state-srv'
30
+ require 'goat/dynamic'
14
31
 
15
- class ActionHash < Hash; end
16
- class ActionProc < Proc; end
32
+ $verbose ||= ARGV.include?('-v')
17
33
 
18
- def action(hash={})
19
- hash
20
- # ActionProc.new { hash }
21
- # ActionHash[hash]
22
- end
34
+ class Profile
35
+ # TODO make sure not already defined
36
+ def self.in(*args); end
37
+ def self.out(*args); end
38
+ def self.request_start(*args); end
39
+ def self.request_end(*args); end
40
+ end
23
41
 
24
- class Object
25
- def self.make_me
26
- meth = nil
27
- if caller[1] =~ /`(.+)'/
28
- meth = $1
29
- end
30
-
31
- raise("Subclass #{self.name} should implement" + (meth ? " #{meth}" : ''))
32
- end
33
42
 
34
- def make_me; self.class.make_me end
35
-
36
- alias_method :old_kind_of?, :kind_of?
37
-
38
- def self.kind_of?(cls)
39
- return true if self.old_kind_of?(Class) && cls == Class
40
- self == cls || \
41
- (self == Object ? \
42
- false : \
43
- (self.superclass && self.superclass != self && self.superclass.kind_of?(cls)))
44
- end
45
-
46
- def glimpse(n=100)
47
- ins = self.inspect
48
- if ins =~ />$/ && ins.size > n
49
- "#{ins[0..n]}...>"
50
- else
51
- ins
52
- end
43
+ class String
44
+ def prefix_ns(ns)
45
+ self.gsub(/^%(.+)$/, "#{ns}_\\1")
53
46
  end
54
- end
55
47
 
56
- module Kernel
57
- # from http://redcorundum.blogspot.com/2006/05/kernelqualifiedconstget.html
58
- def fetch_class(str)
59
- path = str.to_s.split('::')
60
- from_root = path[0].empty?
61
- if from_root
62
- from_root = []
63
- path = path[1..-1]
64
- else
65
- start_ns = ((Class === self)||(Module === self)) ? self : self.class
66
- from_root = start_ns.to_s.split('::')
67
- end
68
- until from_root.empty?
69
- begin
70
- return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
71
- rescue NameError
72
- from_root.delete_at(-1)
73
- end
74
- end
75
- path.inject(Object) { |ns,name| ns.const_get(name) }
48
+ def h!
49
+ # TODO is this even used any more?
50
+ Goat::HTMLString.new(self)
76
51
  end
77
- end
78
52
 
79
- class Set
80
- def glimpse(n=100)
81
- "#<Set: #{self.map{|x| x.glimpse(n)}.join(', ')}>"
53
+ def handle_request(req)
54
+ [200, {}, self]
82
55
  end
83
56
  end
84
57
 
85
- class Array
86
- def glimpse(n=100)
87
- "[" + self.map{|x| x.glimpse(n)}.join(', ') + "]"
88
- end
58
+ def h!(str)
59
+ Goat::HTMLString.new(str)
89
60
  end
90
61
 
91
- class Hash
92
- def glimpse(n=100)
93
- "{" + self.map{|k, v| k.glimpse + "=>" + v.glimpse}.join(', ') + "}"
94
- end
95
- end
62
+ module Goat
63
+ class HTMLString < String; end
96
64
 
97
- class Hash
98
- def map_to_hash
99
- h = {}
100
- self.map do |k, v|
101
- nk, nv = yield(k, v)
102
- h[nk] = nv
65
+ class << self
66
+ inline do |builder|
67
+ builder.c 'VALUE new_without_initialize(VALUE klass) {
68
+ /* The 1 here is a special constant in ruby that corresponds to ID_ALLOCATOR */
69
+ return rb_funcall(klass, 1, 0, 0);
70
+ }'
103
71
  end
104
- h
105
72
  end
106
- end
107
73
 
108
- class String
109
- def prefix_ns(ns)
110
- self.gsub(/^%(.+)$/, "#{ns}_\\1")
111
- end
112
- end
113
-
114
- module Goat
115
74
  def self.version
116
- if @version
117
- @version
118
- elsif !@tried_to_detect_version
119
- @tried_to_detect_version = true
120
- path = File.join(File.dirname(__FILE__), '../goat.gemspec')
121
- if File.exists?(path)
122
- v = File.read(path).split("\n").select{|l| l =~ /version/}.first
123
- if v && v =~ /["']([0-9\.]+)["']/
124
- @version = $1
125
- end
126
- elsif
127
- dir = File.expand_path(File.dirname(__FILE__))
128
- if dir =~ /goat-([0-9\.]+)\//
129
- @version = $1
130
- end
131
- end
132
- else
133
- nil
134
- end
75
+ specpath = File.join(File.dirname(__FILE__), '../goat.gemspec')
76
+ Gem::Specification.load(specpath).version.to_s
135
77
  end
136
-
137
- def self.goat_path(f); File.join(File.dirname(__FILE__), 'goat', f); end
138
78
 
139
- def self.extend_sinatra
140
- require goat_path('sinatra')
141
- end
79
+ def self.goat_path(f); File.join(File.dirname(__FILE__), 'goat', f); end
142
80
 
143
81
  def self.extend_mongo
144
82
  require goat_path('mongo')
145
83
  end
146
-
84
+
147
85
  def self.enable_notifications(opts={})
148
86
  NotificationCenter.configure(opts)
149
87
  end
150
-
88
+
89
+ def self.enable_statesrv(opts={})
90
+ StateSrvClient.configure(opts)
91
+ end
92
+
93
+ def self.enable_appsrv(opts={})
94
+ UpdateDispatcher.enable
95
+ end
96
+
151
97
  def self.load_all(dir_fragment)
152
98
  dir = File.join(Goat.setting!(:root), dir_fragment)
153
99
  if File.directory?(dir)
154
100
  Dir.entries(dir).select{|f| f =~ /\.rb$/}.each {|f| require(File.join(dir, f))}
155
101
  end
156
102
  end
157
-
103
+
158
104
  @settings = {}
159
105
  def self.setting(opt)
160
106
  @settings[opt]
161
107
  end
162
-
163
- def self.setting!(opt)
108
+
109
+ def self.setting!(opt)
164
110
  if @settings.include?(opt)
165
111
  @settings[opt]
166
112
  else
@@ -169,63 +115,57 @@ module Goat
169
115
  end
170
116
 
171
117
  def self.settings; @settings; end
172
-
118
+
173
119
  def self.add_component_helpers(modul)
174
120
  Goat::Component.send(:include, modul)
175
121
  end
176
122
 
177
123
  def self.configure(&blk)
178
124
  blk.call
179
-
125
+
180
126
  load_all('components')
181
- load_all('pages')
182
-
127
+ load_all('pages')
128
+
183
129
  Goat.extend_mongo if Goat.setting(:mongo)
184
- Goat.extend_sinatra if Goat.setting(:sinatra)
185
-
130
+
186
131
  if p = Goat.setting(:press)
187
132
  Goat::Static.press = p
188
133
  end
189
-
134
+
190
135
  if Goat.setting(:debug)
191
136
  if defined?(Thin)
192
137
  Thin::Logging.debug = true
193
138
  end
194
-
195
- Goat::Logger.levels = [:debug, :error]
196
- end
197
-
139
+ end
140
+
198
141
  NotificationCenter.configure(Goat.setting(:notifications)) if Goat.setting(:notifications)
199
142
  end
200
-
143
+
201
144
  def self.rack_builder(app)
202
- Rack::Builder.new do
145
+ Rack::Builder.new do
203
146
  if cookies = Goat.setting(:cookies)
204
147
  use Rack::Session::Cookie, cookies
205
148
  end
206
-
149
+
207
150
  if static = Goat.setting(:static)
208
151
  use Rack::Static, :urls => static.fetch(:urls), :root => static.fetch(:root)
209
152
  end
210
-
153
+
211
154
  use Rack::Flash if defined?(Rack::Flash) # TODO hack
212
-
213
- if Goat.setting(:reload_files)
214
- use Rack::Reloader
215
- end
216
-
155
+ use Rack::CommonLogger
156
+
217
157
  run app
218
158
  end
219
159
  end
220
-
160
+
221
161
  class NotFoundError < RuntimeError
222
162
  attr_reader :path
223
-
163
+
224
164
  def initialize(path)
225
165
  @path = path
226
166
  end
227
167
  end
228
-
168
+
229
169
  class ChannelPusher
230
170
  include EM::Deferrable
231
171
 
@@ -236,7 +176,7 @@ module Goat
236
176
  @messages = []
237
177
 
238
178
  self.errback do
239
- Logger.log :live, "Channel closed"
179
+ logd "Channel closed"
240
180
  @finished = true
241
181
  end
242
182
 
@@ -279,26 +219,26 @@ module Goat
279
219
  @body_callback = blk
280
220
  end
281
221
  end
282
-
222
+
283
223
  class Halt < Exception
284
224
  attr_reader :response
285
-
225
+
286
226
  def initialize(response)
287
227
  @response = response
288
228
  end
289
229
  end
290
230
 
291
231
  class ReqHandler
292
-
293
- # we can't know this at initialize time because we start using
232
+
233
+ # we can't know this at initialize time because we start using
294
234
  # the req handler in context of Goat::App, which would lead to
295
235
  # app = Goat::App, which is wrong
296
236
  attr_accessor :app_class
297
-
237
+
298
238
  def initialize
299
239
  @around_handler_bindings = {}
300
240
  end
301
-
241
+
302
242
  class ::Proc
303
243
  def handle_request(app)
304
244
  self.call(app)
@@ -329,7 +269,7 @@ module Goat
329
269
 
330
270
  mappings[method][path] = hook
331
271
  end
332
-
272
+
333
273
  def around_handler_binding(handler, type)
334
274
  if type == :before
335
275
  @around_handler_bindings[handler] ||= App.bind(handler)
@@ -338,20 +278,20 @@ module Goat
338
278
  else
339
279
  raise 'bad handler type'
340
280
  end
341
- end
342
-
281
+ end
282
+
343
283
  def run_around_handlers(app, type)
344
284
  app.class.around_handlers.select{|h| h.first == type}.each do |handler|
345
285
  around_handler_binding(handler[1], type).call(app)
346
286
  end
347
287
  end
348
-
288
+
349
289
  def run_before_handlers(app); run_around_handlers(app, :before); end
350
290
  def run_after_handlers(app); run_around_handlers(app, :after); end
351
-
352
- def resp_for_error(e, app)
291
+
292
+ def resp_for_error(e, app)
353
293
  resp = nil
354
-
294
+
355
295
  if e.kind_of?(NotFoundError)
356
296
  # 404
357
297
  if app.class.not_found_handler
@@ -360,43 +300,58 @@ module Goat
360
300
  else
361
301
  resp = [404, {}, 'not found']
362
302
  end
363
- else
364
- # not a 404 -- an actual problem
303
+ else
304
+ # not a 404 -- an actual problem
365
305
  if app.class.error_handler
366
306
  @error_handler_binding ||= App.bind(app.class.error_handler)
367
307
  resp = @error_handler_binding.call(app, e)
368
308
  else
369
- Logger.error(:req, e.inspect)
370
- Logger.error(:req, e.backtrace.join("\n"))
371
-
309
+ loge e.inspect
310
+ loge e.backtrace.join("\n")
311
+
372
312
  resp = Rack::Response.new
373
313
  resp.status = 500
374
- resp['Content-Type'] = 'text/plain'
314
+ resp['Content-Type'] = 'text/plain'
375
315
  resp.body = e.inspect + "\n" + e.backtrace.join("\n")
376
316
  end
377
317
  end
378
-
318
+
379
319
  resp
380
320
  end
381
-
382
- def handle_request(env)
321
+
322
+ def find_hook(meth, path)
323
+ hooks = mappings[meth.downcase.to_sym]
324
+ if h = hooks[path]
325
+ return h
326
+ else
327
+ hooks.each do |pathspec, hook|
328
+ if pathspec.kind_of?(Regexp) && pathspec =~ path
329
+ return hook
330
+ end
331
+ end
332
+ end
333
+
334
+ nil
335
+ end
336
+
337
+ def handle_request(env)
383
338
  path = env['PATH_INFO']
384
339
  meth = env['REQUEST_METHOD']
385
- hook = mappings[meth.downcase.to_sym][path]
340
+ hook = find_hook(meth, path)
386
341
  hdrs = {}
387
342
  resp = nil
388
-
343
+
389
344
  begin
390
- req = Rack::Request.new(env)
391
-
345
+ req = Rack::Request.new(env)
346
+
392
347
  app = @app_class.new(req)
393
-
348
+
394
349
  begin
395
350
  run_before_handlers(app)
396
351
  rescue Halt => halt
397
352
  return halt.response.to_a
398
353
  end
399
-
354
+
400
355
  if hook
401
356
  resp = hook.handle_request(app)
402
357
  else
@@ -407,40 +362,40 @@ module Goat
407
362
  rescue Exception => e
408
363
  resp = resp_for_error(e, app)
409
364
  end
410
-
365
+
411
366
  run_after_handlers(app)
412
367
 
413
368
  resp.to_a
414
369
  end
415
370
  end
416
-
371
+
417
372
  module HTMLHelpers
418
373
  include Rack::Utils
419
374
  alias_method :h, :escape_html
420
375
  alias_method :e, :escape # for URIs
421
-
422
- def jsesc(x); x.gsub('\\', '\\\\\\').gsub('"', '\"'); end
423
- end
424
-
376
+
377
+ def jsesc(x); x.gsub('\\', '\\\\\\').gsub('"', '\"'); end
378
+ end
379
+
425
380
  module FlashHelper
426
381
  def flash
427
382
  request.env['x-rack.flash']
428
- end
429
- end
430
-
383
+ end
384
+ end
385
+
431
386
  class ERBRunner
432
387
  include HTMLHelpers
433
388
  include FlashHelper
434
-
389
+
435
390
  def initialize(req, resp, params)
436
391
  @request = req
437
392
  @response = resp
438
393
  @params = params
439
394
  end
440
-
395
+
441
396
  attr_reader :request, :response, :params
442
397
  attr_accessor :delegate
443
-
398
+
444
399
  def take_delegate_ivars
445
400
  @delegate.instance_variables.each do |ivar|
446
401
  unless self.instance_variables.include?(ivar)
@@ -448,7 +403,7 @@ module Goat
448
403
  end
449
404
  end
450
405
  end
451
-
406
+
452
407
  def method_missing(meth, *args)
453
408
  if @delegate
454
409
  @delegate.send(meth, *args)
@@ -456,26 +411,26 @@ module Goat
456
411
  super
457
412
  end
458
413
  end
459
-
414
+
460
415
  def erb(name, opts={}, &blk)
461
416
  take_delegate_ivars if @delegate
462
-
417
+
463
418
  opts = {
464
419
  :partial => false,
465
420
  :layout => true,
466
421
  :locals => {}
467
422
  }.merge(opts)
468
-
423
+
469
424
  if self.kind_of?(Page)
470
425
  opts[:locals][:page] ||= self
471
426
  end
472
-
427
+
473
428
  partial = opts[:partial]
474
429
  use_layout = opts[:layout]
475
430
  locals = opts[:locals]
476
-
431
+
477
432
  if partial
478
- name = name.to_s
433
+ name = name.to_s
479
434
  d = File.dirname(name)
480
435
  d = d == '.' ? '' : "#{d}/"
481
436
  f = File.basename(name)
@@ -483,19 +438,19 @@ module Goat
483
438
  # slashes are actually allowed in syms
484
439
  name = "#{d}_#{f}".to_sym
485
440
  end
486
-
441
+
487
442
  if name =~ /\.erb$/ # allow an absolute path to be passed
488
443
  erbf = name
489
444
  else
490
445
  erbf = File.join(Goat.setting!(:root), 'views', "#{name}.erb")
491
446
  end
492
-
447
+
493
448
  layf = File.join(Goat.setting!(:root), 'views', 'layout.erb')
494
449
  template = Tilt[:erb].new(erbf) { File.read(erbf) }
495
-
450
+
496
451
  layout = File.read(layf) if File.exists?(layf) && !partial && use_layout
497
452
  out = template.render(self, locals, &blk)
498
-
453
+
499
454
  if layout
500
455
  laytpl = Tilt[:erb].new(layf) { layout }
501
456
  laytpl.render(self, locals) { out }
@@ -503,32 +458,29 @@ module Goat
503
458
  out
504
459
  end
505
460
  end
506
-
461
+
507
462
  def partial_erb(name, opts={})
508
463
  erb(name, {:partial => true}.merge(opts))
509
464
  end
510
- end
511
-
465
+ end
466
+
512
467
  module AppHelpers
513
- def halt
468
+ def halt(body=nil)
469
+ response.body = body if body
514
470
  raise Halt.new(response)
515
471
  end
516
-
472
+
517
473
  def redirect(url)
518
474
  response.status = 302
519
475
  response['Location'] = url
520
476
  halt
521
477
  end
522
-
478
+
523
479
  def session
524
480
  request.env['rack.session'] ||= {}
525
- end
526
-
527
- def render_component(c)
528
- c.processed_html
529
481
  end
530
482
  end
531
-
483
+
532
484
  class IndifferentHash < Hash
533
485
  def self.from_hash(hash)
534
486
  ih = self.new
@@ -537,56 +489,26 @@ module Goat
537
489
  end
538
490
  ih
539
491
  end
540
-
492
+
541
493
  def [](k)
542
494
  if k.kind_of?(Symbol)
543
495
  k_sym = k
544
496
  k_str = k.to_s
545
497
  raise 'Invalid hash' if self.include?(k_sym) && self.include?(k_str)
546
-
498
+
547
499
  self.include?(k_str) ? self.fetch(k_str) : self.fetch(k_sym, nil)
548
500
  else
549
501
  super(k)
550
502
  end
551
503
  end
552
504
  end
553
-
505
+
554
506
  class App
555
507
  class << self
556
-
557
- MAX_ACTIVE_PAGES = 100
508
+
558
509
  @@error_handler = nil
559
510
  @@not_found_handler = nil
560
- @@active_pages = {}
561
- @@active_page_queue = Queue.new
562
511
  @@around_handlers = []
563
-
564
- def active_pages; @@active_pages; end
565
- def active_page_queue; @@active_page_queue; end
566
-
567
- def add_active_page(pg)
568
- pgid = pg.id
569
- active_page_queue << pg.id
570
- self.active_pages[pg.id] = pg
571
- end
572
-
573
- def active_page_gc
574
- deleted = 0
575
- while active_page_queue.size > MAX_ACTIVE_PAGES
576
- pgid = active_page_queue.pop
577
- pg = active_pages[pgid]
578
- pg.mark_dead!
579
- NotificationCenter.delegate_gc
580
- active_pages.delete(pgid)
581
- Logger.log :gc, "Removing page #{pgid}"
582
- deleted += 1
583
- end
584
- ObjectSpace.garbage_collect
585
- Logger.log :gc, "Page GC ejected #{deleted} pages"
586
- rescue Exception => e
587
- Logger.error(:gc, e.inspect)
588
- Logger.error(:gc, e.backtrace.join("\n"))
589
- end
590
512
 
591
513
  def req_handler
592
514
  @@reqhandler ||= ReqHandler.new
@@ -605,13 +527,13 @@ module Goat
605
527
  def map(opts)
606
528
  req_handler.add_mapping(opts)
607
529
  end
608
-
609
- def around_handlers; @@around_handlers; end
610
-
530
+
531
+ def around_handlers; @@around_handlers; end
532
+
611
533
  def before(&blk)
612
534
  around_handlers << [:before, blk]
613
535
  end
614
-
536
+
615
537
  def after(&blk)
616
538
  around_handlers << [:after, blk]
617
539
  end
@@ -619,145 +541,101 @@ module Goat
619
541
  def enable_notifications(opts={})
620
542
  NotificationCenter.configure(opts)
621
543
  end
622
-
623
- def debug_pages
624
- active_pages.each do |i, pg|
625
- Logger.log :gc, [i, pg.dead?].inspect
626
- end
627
- end
628
-
629
- def handle_channel(req)
630
- id = req['_id']
631
- jsonp = req['jsonp']
632
- pg = active_pages[id]
633
-
634
- active_page_gc # we do GC here since a slight delay in opening channel is better than in loading page
635
-
636
- if !jsonp
637
- respond_failed
638
- end
639
-
640
- if pg
641
- pg.mark_alive!
642
- body = ChannelPusher.new(pg.channel, jsonp)
643
544
 
644
- EM.next_tick do
645
- req.env['async.callback'].call([200, {'Content-Type' => 'application/javascript'}, body])
646
- end
647
-
648
- body.callback { pg.mark_dead! }
649
- body.errback { pg.mark_dead! }
545
+ def respond_success; [200, {}, ['ok']]; end
546
+ def respond_failed; [500, {}, ['failed']]; end
650
547
 
651
- Logger.log :req, 'respond'
548
+ def handle_rpc(app)
549
+ req = app.request
652
550
 
653
- [-1, {}, []]
654
- else
655
- Logger.error(:req, "Couldn't find page #{id}")
551
+ cls = req['cls']
552
+ cid = req['id']
553
+ rpc = req['rpc']
554
+ txn = req['rttxn']
555
+ pgid = req['pgid']
556
+ reqargs = JSON.load(req['args'])
656
557
 
657
- respond_failed
658
- end
659
- end
558
+ have_handler = false
559
+ resp = nil
660
560
 
661
- def respond_success; [200, {}, ['ok']]; end
662
- def respond_failed; [500, {}, ['failed']]; end
561
+ Goat::NotificationCenter.notify(
562
+ 'type' => 'txn_start',
563
+ 'txn' => txn,
564
+ 'pgid' => pgid
565
+ )
663
566
 
664
- def with_component_from_req(req)
665
- id = req['_id']
666
- compid = req['_component']
667
-
668
- pg = active_pages[id]
669
- if pg
670
- comp = pg.components.detect{|x| x.id == compid}
671
- Logger.error :req, "Couldn't find component #{compid.inspect}" unless comp
672
-
673
- if comp
674
- yield comp
675
- else
676
- respond_failed
677
- end
678
- else
679
- Logger.error(:req, "Couldn't find page #{id}")
567
+ if comp = Goat.rpc_handlers[cls]
568
+ if comp.include?(rpc)
569
+ Dynamic.let(:txn => txn, :txn_pgid => pgid) do
570
+ have_handler = true
571
+ opts = comp[rpc]
572
+ args = []
573
+ if opts[:live]
574
+ if skel = StateSrvClient.fetch_component(cid)
575
+ component = Kernel.fetch_class(cls).from_skel(skel)
576
+ component.deserialize(skel.state)
577
+
578
+ args << component
579
+ else
580
+ return respond_failed
581
+ end
582
+ end
680
583
 
681
- respond_failed
682
- end
683
- end
584
+ args << IndifferentHash.from_hash(reqargs)
684
585
 
685
- def handle_post(req)
686
- with_component_from_req(req) do |comp|
687
- comp.submitted(req)
688
- respond_success
689
- end
690
- end
586
+ resp = app.respond_with_hook("rpc_#{rpc}", *args)
587
+ resp = opts[:is_get] ? resp.to_json : [200, {}, '']
691
588
 
692
- def handle_dispatch(req)
693
- with_component_from_req(req) do |comp|
694
- k = req['_dispatch'];
695
- if comp.callback_registered?(k)
696
- comp.run_callback(k)
697
- respond_success
698
- else
699
- Logger.error(:req, "Couldn't find handler #{k.inspect}")
700
- respond_failed
701
- end
702
- end
703
- end
704
-
705
- def handle_rpc(app)
706
- req = app.request
707
-
708
- c = req['_component']
709
- id = req['_id']
710
- rpc = req['_rpc']
711
-
712
- if comp = Goat.rpc_handlers[c]
713
- if comp[rpc]
714
- return app.respond_with_hook("rpc_#{rpc}", IndifferentHash.from_hash(req.params))
589
+ if opts[:live]
590
+ component.update
591
+ end
592
+ end
715
593
  end
716
594
  end
717
-
718
- [500, {}, {'success' => false}.to_json]
595
+
596
+ Goat::NotificationCenter.notify(
597
+ 'type' => 'txn_complete',
598
+ 'txn' => txn
599
+ )
600
+
601
+ have_handler ? resp : [500, {}, {'success' => false}.to_json]
719
602
  end
720
-
603
+
721
604
  def error(&blk)
722
605
  @@error_handler = blk
723
606
  end
724
-
607
+
725
608
  def error_handler; @@error_handler; end
726
-
609
+
727
610
  def not_found(&blk)
728
611
  @@not_found_handler = blk
729
612
  end
730
-
613
+
731
614
  def not_found_handler; @@not_found_handler; end
732
-
615
+
733
616
  end # end class << self
734
-
617
+
735
618
  def self.call(env)
736
- # TODO better place to put this?
737
- NotificationCenter.init # will do nothing if not enabled
738
-
739
619
  self.req_handler.app_class = self
740
-
741
620
  self.req_handler.handle_request(env)
742
621
  end
743
-
622
+
744
623
  include AppHelpers
745
624
  include FlashHelper
746
-
625
+
747
626
  def self.bind(hook, name=nil, for_response=true)
748
627
  mname = name || String.random
749
-
628
+
750
629
  lambda do |app, *args|
751
630
  kls = app.class
752
-
631
+
753
632
  unless kls.instance_methods.include?(mname)
754
- Logger.log :req, "defining #{mname} on #{kls}"
633
+ logd "defining #{mname} on #{kls}"
755
634
  kls.send(:define_method, mname, hook)
756
635
  hook = kls.instance_method(mname)
757
636
  end
758
-
759
- Logger.log :req, "running #{mname}"
760
- if for_response
637
+
638
+ if for_response
761
639
  # sets the body
762
640
  app.respond_with_hook(mname, *args)
763
641
  else
@@ -765,35 +643,37 @@ module Goat
765
643
  end
766
644
  end
767
645
  end
768
-
646
+
769
647
  def initialize(req, meth=nil)
770
648
  @req = req
771
649
  @meth = meth
772
650
  @response = Rack::Response.new
773
651
  @params = IndifferentHash.from_hash(req.params)
774
652
  end
775
-
653
+
776
654
  def response; @response; end
777
655
  def request; @req; end
778
656
  def params; @params; end
779
-
657
+
780
658
  def respond_with_hook(hook, *args)
781
659
  response.body = self.send(hook, *args) || ''
782
660
  response.finish
783
661
  end
784
-
662
+
785
663
  def erb(name, opts={}, &blk)
786
664
  e = ERBRunner.new(@req, @response, @params)
787
665
  e.delegate = self
788
666
  e.erb(name, {:locals => {:page => nil}}.merge(opts), &blk)
789
667
  end
790
-
791
- map :path => '/channel', :metal => true, :hook => lambda {|app| handle_channel(app.request)}
792
- map :path => '/post', :metal => true, :hook => lambda {|app| handle_post(app.request) }
793
- map :path => '/dispatch', :metal => true, :hook => lambda {|app| handle_dispatch(app.request) }
668
+
794
669
  map :path => '/rpc', :method => :post, :metal => true, :hook => lambda {|app| handle_rpc(app)}
795
670
  end
796
671
 
672
+ class BasicApp < App
673
+ get '/static/jquery.js', File.read(Goat.goat_path('../../examples/jq.js'))
674
+ get '/static/goat.js', File.read(Goat.goat_path('goat.js'))
675
+ end
676
+
797
677
  class Channel
798
678
  # thin wrapper for EM::Channel
799
679
 
@@ -804,331 +684,623 @@ module Goat
804
684
  end
805
685
 
806
686
  def send_message(msg)
807
- Logger.log :live, "Pushing #{msg.inspect}"
808
687
  @emchannel.push(msg)
809
688
  end
810
689
  end
811
-
690
+
812
691
  def self.rpc_handlers; @rpc_handlers ||= {}; end
813
692
 
693
+ class DOMDistiller
694
+ def initialize(dom, cs)
695
+ @dom = dom
696
+ @components = cs
697
+ end
698
+
699
+ def all_components
700
+ @components
701
+ end
702
+
703
+ def all_component_classes
704
+ @all_component_classes ||= \
705
+ all_components.map(&:class).uniq.\
706
+ map{|cls| cls.superclasses(Component)}.uniq.flatten
707
+ end
708
+
709
+ def ordered_component_classes
710
+ cs = all_component_classes
711
+
712
+ i = 0
713
+ # invariant: left of i has no superclasses to right
714
+ while i < cs.size
715
+ c = cs[i]
716
+ if cs[(i+1)..-1].any?{|sup| c.subclass_of?(sup)}
717
+ cs.delete(c)
718
+ cs << c
719
+ else
720
+ i += 1
721
+ end
722
+ end
723
+
724
+ cs
725
+ end
726
+
727
+ def unpressed_component_classes
728
+ ordered_component_classes.to_a.reject do |cls|
729
+ Goat.setting(:press) && Goat::Static.pressed?(cls)
730
+ end
731
+ end
732
+
733
+ def script
734
+ cs = unpressed_component_classes
735
+
736
+ [
737
+ cs.map(&:__script),
738
+ all_components.select{|c| c.class.wired?}.map(&:wire_script),
739
+ all_components.map(&:__script)
740
+ ].flatten.compact.join(';')
741
+ end
742
+
743
+ def style
744
+ unpressed_component_classes.select(&:__css).map(&:scoped_css).join
745
+ end
746
+ end
747
+
814
748
  class Page
815
749
  attr_reader :canvas, :request, :id, :canvas, :layout, :params, :channel
816
-
750
+
817
751
  @live_updates = true
818
-
752
+
819
753
  def self.disable_live_updates; @live_updates = false; end
820
754
  def self.enable_live_updates; @live_updates = true; end
821
755
  def self.live_updates_enabled?; @live_updates != false; end
822
-
756
+
823
757
  include FlashHelper
824
-
758
+
825
759
  def erb(name, opts={}, &blk)
826
760
  e = ERBRunner.new(@request, @response, @params)
827
761
  e.delegate = self
828
762
  e.erb(name, {:partial => false}.merge(opts), &blk)
829
763
  end
830
-
764
+
831
765
  def self.handle_request(app)
832
- pg = self.new(app)
833
- app.class.add_active_page(pg)
834
- resp = pg.response
835
-
836
- # app.class.active_page_gc
837
-
838
- action_hashes = 0
839
-
840
- ObjectSpace.each_object do |obj|
841
- if obj.kind_of?(Page) && obj.class != Class
842
- #puts "Page: #{obj.glimpse}"
843
- elsif obj.kind_of?(Component) && obj.class != Class
844
- #puts "Component: #{obj.glimpse}"
845
- elsif obj.kind_of?(ActionProc) && obj.class != Class
846
- action_hashes += 1
847
- end
848
- end
849
-
850
- puts "ActionHashes: #{action_hashes}"
851
-
852
- resp
766
+ pg = self.new(app)
767
+ pg.response
853
768
  end
854
769
 
855
770
  def initialize(app)
856
771
  @request = app.request
857
772
  @channel = Channel.new
858
- @canvas = PageCanvas.new
859
- @id = String.random(10, :alpha => true)
773
+ @id = 'pg_' + String.random(10)
860
774
  @params = IndifferentHash.from_hash(request.params)
861
775
  end
862
-
863
- def components; @canvas.all_components; end
864
-
776
+
865
777
  def empty_response
866
778
  Rack::Response.new
867
779
  end
868
-
780
+
869
781
  def halt
870
782
  raise Halt.new(empty_response)
871
783
  end
872
-
873
- def dead?
874
- components.first.dead?
875
- end
876
-
877
- def mark_dead!
878
- components.each(&:mark_dead!)
784
+
785
+ def register_page(distiller)
786
+ live = distiller.all_components.select(&:live_enabled?)
787
+ pg_spec = live.map do |c|
788
+ c.pgid = @id
789
+ c.skel
790
+ end
791
+
792
+ StateSrvClient.register_page(@id, pg_spec)
879
793
  end
880
-
881
- def mark_alive!
882
- components.each(&:mark_alive!)
794
+
795
+ def html_from_erb
796
+ html = self.erb(@erb, :layout => false)
883
797
  end
884
-
885
- class CanvasDistiller
886
- def initialize(canvas)
887
- @canvas = canvas
888
- end
889
-
890
- def all_component_classes
891
- comp_classes = Set.new
892
- @canvas.all_components.map(&:class).uniq.map do |cls|
893
- c = cls
894
-
895
- while c
896
- if c == Object
897
- raise "Error traversing class hierarchy to find necessary JS"
898
- end
899
-
900
- comp_classes << c
901
-
902
- if c == Component
903
- break
904
- else
905
- c = c.superclass
906
- end
907
- end
908
- end
909
-
910
- comp_classes
911
- end
912
-
913
- def ordered_component_classes
914
- cs = all_component_classes.to_a
915
-
916
- i = 0
917
- # invariant: left of i has no superclasses to right
918
- while i < cs.size
919
- c = cs[i]
920
- if cs[(i+1)..-1].any?{|sup| c.kind_of?(sup)}
921
- cs.delete(c)
922
- cs << c
923
- else
924
- i += 1
925
- end
926
- end
927
-
928
- cs
929
- end
930
-
931
- def unpressed_component_classes
932
- ordered_component_classes.to_a.reject do |cls|
933
- Goat.setting(:press) && Goat::Static.pressed?(cls)
934
- end
935
- end
936
-
937
- def script
938
- cs = unpressed_component_classes
939
-
940
- [
941
- cs.map(&:__script),
942
- @canvas.all_components.select{|c| c.class.wired?}.map(&:wire_script),
943
- @canvas.all_components.map(&:__script)
944
- ].flatten.compact.join(';')
945
- end
946
-
947
- def style
948
- unpressed_component_classes.select(&:__css).map(&:scoped_css).join
798
+
799
+ def inject_html_from_dom(canvas)
800
+ exp = nil
801
+ helper = ExpansionHelper.new
802
+
803
+ Dynamic.let(:expander => helper) do
804
+ exp = DOMTools.expanded_dom(self.dom)
949
805
  end
806
+
807
+ distiller = DOMDistiller.new(exp, helper.components)
808
+
809
+ canvas.html = DOMTools::HTMLBuilder.new(exp).html
810
+ canvas.style << distiller.style
811
+ canvas.script << distiller.script
812
+
813
+ EM.next_tick { register_page(distiller) }
950
814
  end
951
-
815
+
952
816
  def html
953
817
  layout = self.layout
954
-
955
- distiller = CanvasDistiller.new(@canvas)
956
- html = nil
957
-
958
- if @canvas.erb
959
- html = self.erb(@canvas.erb, :layout => false)
960
- elsif !@canvas.body.empty?
961
- html = (@canvas.body.map do |item|
962
- if item.kind_of?(Component)
963
- item.processed_html
964
- elsif item.kind_of?(String)
965
- item
966
- else
967
- raise "Unknown body item: #{item.inspect}"
968
- end
969
- end).join
818
+
819
+ canvas = PageCanvas.new
820
+ canvas.title = @title
821
+
822
+ if @erb
823
+ canvas.html = html_from_erb
824
+ elsif methods.include?('dom')
825
+ inject_html_from_dom(canvas)
970
826
  else
971
- raise "You can define an erb or append to the body, but not both"
972
- end
973
-
974
- @canvas.style << distiller.style
975
- @canvas.script << distiller.script
976
-
977
- render_to_layout(html, layout)
827
+ raise "You should over-ride Page#dom or supply an erb template"
828
+ end
829
+
830
+ render_to_layout(canvas, layout)
978
831
  end
979
-
980
- def render_component(c)
981
- canvas.components << c
982
- c.processed_html
983
- end
832
+
833
+ #def render_component(c)
834
+ # canvas.components << c
835
+ # c.html
836
+ #end
984
837
 
985
838
  def response
986
839
  Rack::Response.new(self.html, 200, {})
987
840
  end
988
-
989
- def render_to_layout(body, layout)
841
+
842
+ def render_to_layout(canvas, layout)
990
843
  layout ||= File.join(File.dirname(__FILE__), 'views/plain_layout.erb')
991
-
844
+
992
845
  erb(layout,
993
- :locals => {:page => self},
846
+ :locals => {:page => self, :canvas => canvas},
994
847
  # don't want a layout in our layout so we can layout while we layout
995
- :layout => false) do
996
- body
997
- end
998
- end
848
+ :layout => false) { canvas.html }
849
+ end
850
+
851
+ def erb=(erb); @erb = erb; #TODO make it work properly, but not support embenned components
852
+ end
853
+ def title=(title); @title = title; end
999
854
  end
1000
855
 
1001
856
  class PageCanvas
1002
- attr_accessor :body, :title, :components, :erb, :script, :style
857
+ attr_accessor :title, :script, :style, :html
1003
858
 
1004
859
  def initialize
1005
- @body = []
1006
- @components = []
1007
860
  @script = []
1008
861
  @style = []
1009
862
  end
1010
-
863
+
1011
864
  def flattened_style; @style.join; end
1012
865
  def flattened_script; @script.join; end
1013
-
1014
- def all_components
1015
- cs = body.empty? ? @components : @body.select{|c| c.kind_of?(Component)}
1016
- cs.map(&:all_components).flatten
866
+ end
867
+
868
+ class JustRerenderError < RuntimeError; end
869
+
870
+ class DOMDiff
871
+ def self.diff(old, new, id)
872
+ self.new(old, new, id).diff
873
+ end
874
+
875
+ def initialize(old, new, id)
876
+ @old = old
877
+ @new = new
878
+ @id = id
879
+ @diffs = []
880
+ end
881
+
882
+ def diff
883
+ nested_application(minimized_changes(desc(@old, @new, @id)))
884
+ end
885
+
886
+ def nested_application(ch)
887
+ applied = Set.new
888
+
889
+ ch.sort_by{|x| x[3]}.each do |c|
890
+ if (c[0] == :rem || c[0] == :add) && !applied.include?(c)
891
+ if n = ch.detect{|x| x != c && x[2] == c[2] && x[3] >= c[3]}
892
+ n[3] += (c[0] == :rem) ? -1 : 1
893
+ end
894
+ end
895
+
896
+ applied << c
897
+ end
898
+
899
+ ch
900
+ end
901
+
902
+ def minimized_changes(ch)
903
+ new = []
904
+ merged = Set.new
905
+ ch.each do |c|
906
+ if c[0] == :rem
907
+ if n = ch.detect{|x| x[0] == :add && x[2] == c[2] && x[3] == c[3]}
908
+ new << [:rep, *n[1..3]]
909
+ merged << n
910
+ else
911
+ new << c
912
+ end
913
+ else
914
+ new << c
915
+ end
916
+ end
917
+ new.reject{|c| merged.include?(c)}
918
+ end
919
+
920
+ def added(new, par, pos=nil); [:add, new, par, pos]; end
921
+ def removed(old, par, pos=nil); [:rem, old, par, pos]; end
922
+
923
+ def dom_node?(node)
924
+ node.is_a?(Array) && node.first.is_a?(Symbol)
925
+ end
926
+ def tag(node); node[0]; end
927
+ def attrs(node); node[1] if node[1].is_a?(Hash); end
928
+ def body(node); node[1].is_a?(Hash) ? node[2..-1] : node[1..-1]; end
929
+ def domid(node); attrs(node) ? attrs(node)[:id] : nil; end
930
+
931
+ def localized_change(dold, dnew, par, changes)
932
+ old = dold.element
933
+ new = dnew.element
934
+ #$stderr.puts "changes: #{changes.inspect} / #{old.inspect} / #{par.inspect}"
935
+ if changes.all?{|ch| ch[2].nil?} && par
936
+ [added(new, par, dold.position), removed(old, par, dnew.position)]
937
+ else
938
+ changes
939
+ end
940
+ end
941
+
942
+ def is_replacement?(d1, d2)
943
+ d1 && d2 && (
944
+ (d1.action == '+' && d2.action == '-') ||
945
+ (d1.action == '-' && d2.action == '+'))
946
+ end
947
+
948
+ def old_and_new(d1, d2)
949
+ d1.action == '+' ? [d2, d1] : [d1, d2]
950
+ end
951
+
952
+ def array_desc(old, new, par)
953
+ #$stderr.puts "array_desc #{old.inspect} #{new.inspect} #{par.inspect}"
954
+
955
+ chgs = Diff::LCS.diff(old, new).map do |diff|
956
+ #$stderr.puts "diff: #{diff.inspect}"
957
+ if is_replacement?(diff[0], diff[1])
958
+ dold, dnew = old_and_new(diff[0], diff[1])
959
+ old = dold.element
960
+ new = dnew.element
961
+ if dom_node?(old) && dom_node?(new) && tag(old) == tag(new) && compare_attrs(old, new)
962
+ localized_change(dold, dnew, par, desc(body(old), body(new), domid(old)))
963
+ elsif old.is_a?(Array) && new.is_a?(Array) #&& old.all?{|x| !dom_node?(x)} && new.all?{|x| !dom_node?(x)}
964
+ array_desc(old, new, par)
965
+ else
966
+ [added(new, par, diff[1].position), removed(old, par, diff[0].position)]
967
+ end
968
+ else
969
+ if diff.size == 1
970
+ diff = diff[0]
971
+ if diff.action == '+'
972
+ [added(diff.element, par, diff.position)]
973
+ elsif diff.action == '-'
974
+ [removed(diff.element, par, diff.position)]
975
+ else
976
+ raise "Don't understand diff in a bad way"
977
+ end
978
+ else
979
+ $stderr.puts "Don't understand diff"
980
+ #$stderr.puts "Diff failed"
981
+ raise JustRerenderError
982
+ end
983
+ end
984
+ end
985
+
986
+ chgs.flatten(1)
987
+ end
988
+
989
+ def compare_attrs(a, b)
990
+ if a.is_a?(Hash) && b.is_a?(Hash)
991
+ a_, b_ = a.clone, b.clone
992
+ a.select{|k,v| a_.delete(k) if v =~ /^dom_/}
993
+ b.select{|k,v| b_.delete(k) if v =~ /^dom_/}
994
+ a_ == b_
995
+ else
996
+ false
997
+ end
998
+ end
999
+
1000
+ def node_desc(old, new, par)
1001
+ #$stderr.puts "node_desc #{old.inspect} #{new.inspect} #{par.inspect}"
1002
+
1003
+ if tag(old) != tag(new) || !compare_attrs(old, new)
1004
+ [removed(old, par), added(new, par)]
1005
+ else # only body changed (maybe)
1006
+ bold = body(old)
1007
+ bnew = body(new)
1008
+ if bold && bnew
1009
+ desc(bold, bnew, domid(old))
1010
+ elsif bold && !bnew
1011
+ [removed(bold, domid(old))]
1012
+ else
1013
+ [added(bnew, domid(old))]
1014
+ end
1015
+ end
1016
+ end
1017
+
1018
+ def desc(old, new, par)
1019
+ #$stderr.puts "desc #{old.inspect} #{new.inspect} #{par.inspect}"
1020
+ #$stderr.puts "desc #{self.class.name} #{par.inspect}"
1021
+
1022
+ if old.class != new.class
1023
+ [added(new, par), removed(old, par)]
1024
+ elsif old.is_a?(Array)
1025
+ dom_node?(old) ? node_desc(old, new, domid(old)) : array_desc(old, new, par)
1026
+ elsif old.is_a?(String)
1027
+ if old != new
1028
+ [added(new, par), removed(old, par)]
1029
+ end
1030
+ else
1031
+ raise TypeError.new("Unknown object in the DOM: #{old.class} #{new.class}")
1032
+ end
1017
1033
  end
1018
1034
  end
1019
1035
 
1020
- class Component
1021
- attr_reader :id, :handlers, :page, :params
1022
-
1023
- include HTMLHelpers # sure just shit it in with everything else
1024
-
1025
- def initialize(page)
1026
- @id = String.random(10, :alpha => true)
1027
- @page = page
1028
- @callbacks = {}
1029
- @params = page.params if page
1030
- @dead = false
1031
- end
1032
-
1033
- def erb(*args, &blk)
1034
- ERBRunner.new(@page.request, nil, @params).erb(*args, &blk)
1036
+ class ExpansionHelper
1037
+ attr_reader :components
1038
+
1039
+ def initialize
1040
+ @components = Set.new
1035
1041
  end
1036
-
1037
- def partial_erb(*args, &blk)
1038
- ERBRunner.new(@page.request, nil, @params).partial_erb(*args, &blk)
1042
+
1043
+ def component_used(c)
1044
+ @components << c
1039
1045
  end
1040
-
1041
- def halt; @page.halt; end
1046
+ end
1042
1047
 
1043
- def channel; @page.channel; end
1044
- def id; @id; end
1048
+ class UpdateDispatcher
1049
+ def self.enable
1050
+ Goat::NotificationCenter.subscribe(self, :txn_start, 'type' => 'txn_start')
1051
+ Goat::NotificationCenter.subscribe(self, :txn_complete, 'type' => 'txn_complete')
1052
+ end
1053
+
1054
+ @active_txns = {}
1055
+
1056
+ def self.txn_start(n)
1057
+ @active_txns[n['txn']] = {
1058
+ :page_updates => [],
1059
+ :other_updates => [],
1060
+ :pgid => n['pgid']
1061
+ }
1062
+ end
1063
+
1064
+ def self.txn_complete(n)
1065
+ finish_txn(n['txn'])
1066
+ end
1067
+
1068
+ def self.finish_txn(txn_id)
1069
+ if txn = @active_txns[txn_id]
1070
+ pgid = txn[:pgid]
1071
+ dispatch_updates(txn_id, pgid, txn[:page_updates])
1072
+ txn[:other_updates].each{|u| dispatch_update(u)}
1073
+ end
1074
+ end
1045
1075
 
1046
- def render
1047
- page.channel.send_message(
1048
- :type => :update,
1049
- :id => id,
1050
- :html => processed_html
1076
+ def self.dispatch_updates(txn, pgid, ups)
1077
+ StateSrvClient.components_updated(
1078
+ txn,
1079
+ pgid,
1080
+ ups
1051
1081
  )
1052
1082
  end
1053
-
1054
- def page_error(msg)
1055
- page.channel.send_message(
1056
- :type => :alert,
1057
- :message => msg
1083
+
1084
+ def self.dispatch_update(update)
1085
+ StateSrvClient.component_updated(nil, update.skel.pgid, update)
1086
+ end
1087
+
1088
+ def self.component_updated(pgid, update)
1089
+ if Dynamic.variable?(:txn)
1090
+ txn = Dynamic[:txn]
1091
+ txn_pgid = Dynamic[:txn_pgid]
1092
+ if updates = @active_txns[txn]
1093
+ if pgid == txn_pgid
1094
+ updates[:page_updates] << update
1095
+ else
1096
+ updates[:other_updates] << update
1097
+ end
1098
+ else
1099
+ raise "Got an update for an un-started txn (#{txn.inspect}, #{pgid})"
1100
+ end
1101
+ else
1102
+ dispatch_update(update)
1103
+ end
1104
+ end
1105
+ end
1106
+
1107
+ class Component
1108
+ attr_reader :id, :handlers, :page, :params, :initargs
1109
+
1110
+ include HTMLHelpers # sure just shit it in with everything else - TODO remove?
1111
+
1112
+ def self.live_enabled; @live_enabled = true; end
1113
+ def self.live_enabled?; @live_enabled; end
1114
+ def live_enabled?; self.class.live_enabled?; end
1115
+
1116
+ def self.rerender_and_update(spec)
1117
+ cls = self.name
1118
+
1119
+ Profile.request_start
1120
+
1121
+ Profile.in(:render_and_update)
1122
+ StateSrvClient.live_components(cls, spec).each do |skel|
1123
+ Profile.in(:cls_rerender)
1124
+ rerender(skel)
1125
+ Profile.out(:cls_rerender)
1126
+ end
1127
+ Profile.out(:render_and_update)
1128
+
1129
+ Profile.request_end
1130
+ end
1131
+
1132
+ def self.rerender(skel)
1133
+ Profile.in(:create_component)
1134
+ c = Kernel.fetch_class(skel.cls).from_skel(skel)
1135
+ c.deserialize(skel.state)
1136
+ Profile.out(:create_component)
1137
+
1138
+ c.rerender
1139
+ end
1140
+
1141
+ def update
1142
+ rerender
1143
+ end
1144
+
1145
+ def rerender
1146
+ Profile.in(:rerender)
1147
+
1148
+ helper = ExpansionHelper.new
1149
+
1150
+ Profile.in(:expansion)
1151
+ Dynamic.let(:expander => helper) do
1152
+ @expanded_dom = self.expanded_dom
1153
+ end
1154
+ Profile.out(:expansion)
1155
+
1156
+ Profile.in(:diff)
1157
+ diff = DOMDiff.diff(@old_dom, @expanded_dom, @id)
1158
+ Profile.out(:diff)
1159
+
1160
+ if diff.size == 0
1161
+ # pass
1162
+ $stderr.puts "No changes"
1163
+ elsif diff.size < 0 # TODO constant
1164
+ rerender_partially(diff)
1165
+ else
1166
+ rerender_fully(helper.components)
1167
+ end
1168
+ rescue JustRerenderError # partial updater gave up
1169
+ rerender_fully(helper.components)
1170
+ ensure
1171
+ Profile.out(:rerender)
1172
+ end
1173
+
1174
+ def pgid=(id); @pgid = id; end
1175
+
1176
+ def skel
1177
+ ComponentSkeleton.new(
1178
+ @pgid,
1179
+ self.class.name,
1180
+ @id,
1181
+ live_spec,
1182
+ serialize,
1183
+ @expanded_dom
1058
1184
  )
1059
1185
  end
1060
1186
 
1061
- def redirect(loc)
1062
- channel.send_message(
1063
- :type => :redirect,
1064
- :location => loc
1187
+ def self.from_skel(skel)
1188
+ inst = Goat.new_without_initialize(self)
1189
+ inst.load_skel(skel)
1190
+ inst
1191
+ end
1192
+
1193
+ def load_skel(skel)
1194
+ @id = skel.id
1195
+ @pgid = skel.pgid
1196
+ @old_dom = skel.dom
1197
+ @live_spec = skel.spec
1198
+ end
1199
+
1200
+ def rerender_partially(diff)
1201
+ $stderr.puts "should do #{diff.inspect} in #{self.class.name}/#{@id}"
1202
+ StateSrvClient.component_updated(
1203
+ self.class.name,
1204
+ @id,
1205
+ @expanded_dom
1065
1206
  )
1066
1207
  end
1067
-
1068
- def mark_dead!
1069
- @dead = true
1208
+
1209
+ def rerender_fully(cs)
1210
+ Profile.in(:distillation)
1211
+ distiller = DOMDistiller.new(@expanded_dom, cs + [self])
1212
+ js = distiller.script
1213
+ css = distiller.style
1214
+ Profile.out(:distillation)
1215
+
1216
+ Profile.in(:update_send)
1217
+
1218
+ UpdateDispatcher.component_updated(
1219
+ @pgid,
1220
+ ComponentUpdate.new(
1221
+ self.skel,
1222
+ [{'type' => 'rep',
1223
+ 'html' => dom_html(@expanded_dom),
1224
+ 'js' => js,
1225
+ 'css' => css,
1226
+ 'parent' => @id,
1227
+ 'position' => nil}]))
1228
+
1229
+ Profile.out(:update_send)
1070
1230
  end
1071
-
1072
- def mark_alive!
1073
- @dead = false
1231
+
1232
+ def initialize
1233
+ @id = 'dom_' + String.random(10)
1074
1234
  end
1075
-
1076
- def dead?; @dead; end
1077
1235
 
1078
- def submit_form(name)
1079
- "Goat.submitForm('#{name.prefix_ns(@id)}')"
1236
+ def live_for(spec)
1237
+ @live_spec = spec
1080
1238
  end
1081
1239
 
1082
- def register_handler(target, selector)
1083
- register_callback { target.send(selector) }
1240
+ def serialize
1241
+ {}
1084
1242
  end
1085
1243
 
1086
- def callback_registered?(k); @callbacks.include?(k); end
1244
+ def deserialize(state)
1245
+ end
1087
1246
 
1088
- def run_callback(k)
1089
- # @callbacks[k].call
1090
- c = @callbacks[k].call
1091
- c[:target].send(c[:method], *c[:args])
1247
+ def live_spec
1248
+ @live_spec ? @live_spec : raise("No live_spec specified")
1092
1249
  end
1093
-
1250
+
1251
+ def erb(*args, &blk)
1252
+ ERBRunner.new(nil, nil, @params).erb(*args, &blk)
1253
+ end
1254
+
1255
+ def partial_erb(*args, &blk)
1256
+ ERBRunner.new(nil, nil, @params).partial_erb(*args, &blk)
1257
+ end
1258
+
1259
+ def id; @id; end
1260
+
1094
1261
  def self.script(script); @script = AutoBind.process(script); end
1095
1262
  def script(script); @script = AutoBind.process(script); end
1096
- def self.__script; @script; end
1263
+ def self.__script; @script; end
1097
1264
  def __script; @script; end
1098
-
1265
+
1099
1266
  def wire_script
1100
1267
  "Goat.wireComponent('#{id}', #{clientside_instance})"
1101
1268
  end
1102
-
1269
+
1103
1270
  def self.css(css); @css = css; end
1104
- def css(css); @css = css; end
1271
+ def css(css); @css = css; end
1105
1272
  def self.__css; @css; end
1106
- def __css; @css; end
1107
-
1273
+ def __css; @css; end
1274
+
1108
1275
  def self.clientside(js)
1109
1276
  script(js)
1110
1277
  @wired = true
1111
1278
  end
1112
-
1279
+
1113
1280
  def self.wired?; @wired; end
1114
-
1281
+
1115
1282
  def clientside_args; []; end
1116
-
1283
+
1117
1284
  def clientside_instance
1118
- args = [id, *clientside_args].to_json[1..-2]
1119
- "new #{self.class.name}('#{self.class.name}', #{args})"
1285
+ args = [id, clientside_args].to_json[1..-2]
1286
+ "(new #{self.class.name}('#{self.class.name}', #{args}))"
1120
1287
  end
1121
-
1122
- def self.rpc(name, &blk)
1288
+
1289
+ def self.rpc(name, opts={}, &blk)
1123
1290
  Goat.rpc_handlers[self.name.to_s] ||= {}
1124
- Goat.rpc_handlers[self.name.to_s][name.to_s] = true
1125
-
1126
- App.send(:define_method, "rpc_inner_#{name}", blk)
1127
- App.send(:define_method, "rpc_#{name}") do |params|
1128
- self.send("rpc_inner_#{name}", params).to_json
1129
- end
1291
+ Goat.rpc_handlers[self.name.to_s][name.to_s] = opts
1292
+
1293
+ App.send(:define_method, "rpc_#{name}", blk)
1294
+ end
1295
+
1296
+ def self.live_rpc(name, opts={}, &blk)
1297
+ rpc(name, opts.merge(:live => true), &blk)
1298
+ end
1299
+
1300
+ def self.get_rpc(name, opts={}, &blk)
1301
+ rpc(name, opts.merge(:is_get => true), &blk)
1130
1302
  end
1131
-
1303
+
1132
1304
  def component_name_hierarchy(cls=self.class)
1133
1305
  if cls == Component
1134
1306
  []
@@ -1136,18 +1308,28 @@ module Goat
1136
1308
  component_name_hierarchy(cls.superclass) + [cls.name]
1137
1309
  end
1138
1310
  end
1139
-
1311
+
1312
+ def inject_prefixes(dom)
1313
+ DOMTools.inject_prefixes(self.id, dom)
1314
+ end
1315
+
1140
1316
  def component(body)
1141
- [:div, {:id => id, :class => component_name_hierarchy.join(' ')}, body]
1317
+ [:div, {:id => id, :class => component_name_hierarchy.join(' ')},
1318
+ body]
1319
+ end
1320
+
1321
+ def expanded_dom
1322
+ @expanded_dom ||= DOMTools.expanded_dom(inject_prefixes(self.dom))
1142
1323
  end
1143
1324
 
1144
- def html; make_me; end
1325
+ def dom_as_expanded
1326
+ @expanded_dom
1327
+ end
1145
1328
 
1146
- def processed_html
1147
- HTMLBuilder.new(self, component(self.html)).html + \
1148
- (__css ? "<style>#{scoped_css}</style>" : '')
1329
+ def dom_html(dom)
1330
+ DOMTools::HTMLBuilder.new(dom).html
1149
1331
  end
1150
-
1332
+
1151
1333
  def self.scope_css(css, prefix, dot_or_hash)
1152
1334
  # #%foo, .%bar, etc
1153
1335
  rep = css.gsub(/(.)%(\w+)([\ \t\{])/) do |str|
@@ -1158,13 +1340,13 @@ module Goat
1158
1340
  end
1159
1341
  rep.gsub(/(^|\W)%([\W\{])/) do |str|
1160
1342
  "#{$1}#{dot_or_hash}#{prefix}#{$2}"
1161
- end
1343
+ end
1162
1344
  end
1163
1345
 
1164
1346
  def self.scoped_css
1165
1347
  scope_css(self.__css, self.name, '.')
1166
1348
  end
1167
-
1349
+
1168
1350
  def scoped_css
1169
1351
  self.class.scope_css(self.__css, @id, '#')
1170
1352
  end
@@ -1172,7 +1354,7 @@ module Goat
1172
1354
  def model_changed(item, notif)
1173
1355
  render
1174
1356
  end
1175
-
1357
+
1176
1358
  def register_callback(callback)
1177
1359
  # if @callbacks.values.include?(callback)
1178
1360
  # @callbacks.to_a.detect{|k, v| k if v == callback}
@@ -1182,33 +1364,7 @@ module Goat
1182
1364
  key
1183
1365
  # end
1184
1366
  end
1185
-
1186
- def children; []; end
1187
-
1188
- def all_components
1189
- [self] + self.children
1190
- end
1191
-
1192
- def to_html(builder, comp)
1193
- processed_html # for html.rb
1194
- end
1195
-
1196
- script File.read(File.join(File.dirname(__FILE__), 'goat/js/component.js'))
1197
- end
1198
-
1199
- class Model
1200
- def initialize
1201
- @delegates = Set.new
1202
- end
1203
-
1204
- def data_changed(notif=nil)
1205
- delegates.each {|d| d.model_changed(self, notif)}
1206
- end
1207
1367
 
1208
- def delegates; @delegates; end
1209
-
1210
- def add_delegate(d)
1211
- @delegates << d
1212
- end
1368
+ script File.read(File.join(File.dirname(__FILE__), 'goat/js/component.js'))
1213
1369
  end
1214
1370
  end