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