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.
@@ -0,0 +1,53 @@
1
+ module Goat
2
+ class ComponentSkeleton
3
+ attr_reader :pgid, :id, :spec
4
+ attr_accessor :dom, :state
5
+
6
+ def cls; @class; end
7
+
8
+ def initialize(pgid, cls, id, spec, state, dom)
9
+ @pgid = pgid
10
+ @class = cls
11
+ @id = id
12
+ @state = state
13
+ @dom = dom
14
+ @spec = spec
15
+ end
16
+
17
+ def self.from_hash(h)
18
+ self.new(h['pgid'], h['class'], h['id'], h['spec'], h['state'], Marshal.load(h['dom']))
19
+ end
20
+
21
+ def to_hash
22
+ {
23
+ 'pgid' => @pgid,
24
+ 'class' => @class,
25
+ 'id' => @id,
26
+ 'spec' => @spec,
27
+ 'state' => @state,
28
+ 'dom' => Marshal.dump(@dom),
29
+ }
30
+ end
31
+ end
32
+
33
+ class ComponentUpdate
34
+ attr_reader :skel, :mutations
35
+ attr_accessor :version
36
+
37
+ def initialize(skel, mutations)
38
+ @skel = skel
39
+ @mutations = mutations
40
+ end
41
+
42
+ def to_hash
43
+ {
44
+ 'skel' => @skel.to_hash,
45
+ 'mutations' => @mutations
46
+ }
47
+ end
48
+
49
+ def self.from_hash(h)
50
+ self.new(ComponentSkeleton.from_hash(h['skel']), h['mutations'])
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,91 @@
1
+ # This library was created by Christian Neukirchen in the context of
2
+ # EuRuKo 2005 and is licensed under the same terms as Ruby.
3
+ #
4
+ # It provides dynamically scoped variables. It is used within ContextR to
5
+ # store the current, thread-wide activated layers.
6
+ #
7
+ # For more information see the corresponding slides at
8
+ # http://chneukirchen.org/talks/euruko-2005/chneukirchen-euruko2005-contextr.pdf
9
+ #
10
+ # (c) 2005 - Christian Neukirchen - http://chneukirchen.org
11
+ module Dynamic
12
+ module ClassMethods #:nodoc:
13
+ Thread.main[:DYNAMIC] = Hash.new { |hash, key|
14
+ raise NameError, "no such dynamic variable: #{key}"
15
+ }
16
+
17
+ def here!
18
+ Thread.current[:DYNAMIC] = Hash.new { |hash, key|
19
+ raise NameError, "no such dynamic variable: #{key}"
20
+ }.update Thread.main[:DYNAMIC]
21
+ end
22
+
23
+ def variable?(v)
24
+ variables.include?(v)
25
+ end
26
+
27
+ def variables
28
+ Thread.current[:DYNAMIC] or here!
29
+ end
30
+
31
+ def variable(definition)
32
+ case definition
33
+ when Symbol
34
+ #if variables.has_key? definition
35
+ # raise NameError, "dynamic variable `#{definition}' already exists"
36
+ #end
37
+ variables[definition] = nil
38
+ when Hash
39
+ definition.each { |key, value|
40
+ if variables.has_key? key
41
+ raise NameError, "dynamic variable `#{key}' already exists"
42
+ end
43
+ variables[key] = value
44
+ }
45
+ else
46
+ raise ArgumentError,
47
+ "can't create a new dynamic variable from #{definition.class}"
48
+ end
49
+ end
50
+
51
+ def [](key)
52
+ variables[key]
53
+ end
54
+
55
+ def []=(key, value)
56
+ variables[key] = value
57
+ end
58
+
59
+ def undefine(*keys)
60
+ keys.each { |key|
61
+ self[key]
62
+ variables.delete key
63
+ }
64
+ end
65
+
66
+ def let(bindings, &block)
67
+ save = {}
68
+ save_keys = variables.keys
69
+ bindings.to_hash.collect { |key, value|
70
+ self.variable(key)
71
+ save[key] = self[key]
72
+ self[key] = value
73
+ }
74
+ block.call
75
+ ensure
76
+ variables.update save
77
+ undefine(*(variables.keys - save_keys))
78
+ end
79
+
80
+ def method_missing(name, *args)
81
+ if match = /=\Z/.match(name.to_s) # setter?
82
+ raise ArgumentError, "invalid setter call" unless args.size == 1
83
+ self[match.pre_match.intern] = args.first
84
+ else
85
+ raise ArgumentError, "invalid getter call" unless args.empty?
86
+ self[name]
87
+ end
88
+ end
89
+ end
90
+ extend ClassMethods
91
+ end
data/lib/goat/extn.rb CHANGED
@@ -16,11 +16,100 @@ class String
16
16
  unless self.instance_methods.include?('random')
17
17
  def self.random(len=10, opts={})
18
18
  opts = {:alpha => false}.merge(opts)
19
- chars = ["a".."z", "A".."Z"]
20
- chars << ("0".."9") unless opts[:alpha]
21
19
 
22
- flat = chars.map(&:to_a).flatten
23
- len.times_collect {flat[SecureRandom.random_number(flat.size)]}.join
20
+ regexp = opts[:alpha] ? /[^A-Za-z]/ : /[^A-Za-z0-9]/
21
+
22
+ str = SecureRandom.random_bytes(len * 8).gsub(regexp, '')
23
+ if str.size >= len
24
+ str[0..(len-1)]
25
+ else
26
+ # should basically never happen
27
+ random(len, opts)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ class Object
34
+ def self.superclasses(upto=Object)
35
+ cs = []
36
+ c = self
37
+ start = self
38
+
39
+ while c
40
+ cs << c
41
+ return cs if c == upto
42
+ raise "#{upto} isn't in the hierachy of #{start.inspect}" if c == Object && upto != Object
43
+
44
+ c = c.superclass
45
+ end
46
+ end
47
+
48
+ def subclass_of?(cls)
49
+ self == cls || \
50
+ (self == Object ? \
51
+ false : \
52
+ (self.superclass && self.superclass != self && self.superclass.subclass_of?(cls)))
53
+ end
54
+
55
+ def glimpse(n=100)
56
+ ins = self.inspect
57
+ if ins =~ />$/ && ins.size > n
58
+ "#{ins[0..n]}...>"
59
+ else
60
+ ins
24
61
  end
25
62
  end
26
- end
63
+ end
64
+
65
+ module Kernel
66
+ # from http://redcorundum.blogspot.com/2006/05/kernelqualifiedconstget.html
67
+ def fetch_class(str)
68
+ path = str.to_s.split('::')
69
+ from_root = path[0].empty?
70
+ if from_root
71
+ from_root = []
72
+ path = path[1..-1]
73
+ else
74
+ start_ns = ((Class === self)||(Module === self)) ? self : self.class
75
+ from_root = start_ns.to_s.split('::')
76
+ end
77
+ until from_root.empty?
78
+ begin
79
+ return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
80
+ rescue NameError
81
+ from_root.delete_at(-1)
82
+ end
83
+ end
84
+ path.inject(Object) { |ns,name| ns.const_get(name) }
85
+ end
86
+ end
87
+
88
+ class Set
89
+ def glimpse(n=100)
90
+ "#<Set: #{self.map{|x| x.glimpse(n)}.join(', ')}>"
91
+ end
92
+ end
93
+
94
+ class Array
95
+ def glimpse(n=100)
96
+ "[" + self.map{|x| x.glimpse(n)}.join(', ') + "]"
97
+ end
98
+ end
99
+
100
+ class Hash
101
+ def glimpse(n=100)
102
+ "{" + self.map{|k, v| k.glimpse + "=>" + v.glimpse}.join(', ') + "}"
103
+ end
104
+ end
105
+
106
+ class Hash
107
+ def map_to_hash
108
+ h = {}
109
+ self.map do |k, v|
110
+ nk, nv = yield(k, v)
111
+ h[nk] = nv
112
+ end
113
+ h
114
+ end
115
+ end
data/lib/goat/goat.js CHANGED
@@ -1,6 +1,322 @@
1
+ (function(){
2
+ var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
3
+ // The base Class implementation (does nothing)
4
+ this.Class = function(){};
5
+
6
+ // Create a new Class that inherits from this class
7
+ Class.extend = function(prop) {
8
+ var _super = this.prototype;
9
+
10
+ // Instantiate a base class (but only create the instance,
11
+ // don't run the init constructor)
12
+ initializing = true;
13
+ var prototype = new this();
14
+ initializing = false;
15
+
16
+ // Copy the properties over onto the new prototype
17
+ for (var name in prop) {
18
+ // Check if we're overwriting an existing function
19
+ prototype[name] = typeof prop[name] == "function" &&
20
+ typeof _super[name] == "function" && fnTest.test(prop[name]) ?
21
+ (function(name, fn){
22
+ return function() {
23
+ var tmp = this._super;
24
+
25
+ // Add a new ._super() method that is the same method
26
+ // but on the super-class
27
+ this._super = _super[name];
28
+
29
+ // The method only need to be bound temporarily, so we
30
+ // remove it when we're done executing
31
+ var ret = fn.apply(this, arguments);
32
+ this._super = tmp;
33
+
34
+ return ret;
35
+ };
36
+ })(name, prop[name]) :
37
+ prop[name];
38
+ }
39
+
40
+ // The dummy class constructor
41
+ function Class() {
42
+ // All construction is actually done in the init method
43
+ if ( !initializing && this.init )
44
+ this.init.apply(this, arguments);
45
+ }
46
+
47
+ // Populate our constructed prototype object
48
+ Class.prototype = prototype;
49
+
50
+ // Enforce the constructor to be what we expect
51
+ Class.constructor = Class;
52
+
53
+ // And make this class extendable
54
+ Class.extend = arguments.callee;
55
+
56
+ return Class;
57
+ };
58
+ })();
59
+
60
+ String.random = function string_random(len, pref) {
61
+ var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
62
+ var str = '';
63
+ for (var i=0; i < len; i++) {
64
+ var r = Math.floor(Math.random() * chars.length);
65
+ str += chars.substring(r, r+1);
66
+ }
67
+ return pref ? (pref + '_' + str) : str;
68
+ }
69
+
70
+ $(document).ready(function() {
71
+ Goat.loadComponents();
72
+ setTimeout(function() { if(Goat.page_id) Goat.openChannel(); }, 1000);
73
+ });
74
+
75
+ var GoatRT = {
76
+ version: 0,
77
+ pendingTxns: {},
78
+ ops: {}
79
+ };
80
+
81
+ (function(rt) {
82
+ function updateFailed(msg) {
83
+ console.error(msg);
84
+ }
85
+
86
+ function node(par, pos) {
87
+ if(pos === null)
88
+ return $('#' + par);
89
+ else
90
+ return $("#" + par + " > :nth-child(" + (pos + 1) + ")");
91
+ }
92
+
93
+ function removeNode(par, pos, html) {
94
+ node(par, pos).remove();
95
+ }
96
+
97
+ function addNode(par, pos, html) {
98
+ node(par, pos).before(html);
99
+ }
100
+
101
+ function replaceNode(par, pos, html) {
102
+ console.log("rep " + par + " " + pos);
103
+ console.log(node(par, pos));
104
+ node(par, pos).html(html);
105
+ if(pos === null)
106
+ delete Goat.components[par];
107
+ }
108
+
109
+ function applyUpdate(up) {
110
+ var t = up.type;
111
+ var par = up.parent;
112
+ var pos = up.position;
113
+ var html = up.html;
114
+ var js = up.js;
115
+ var css = up.css;
116
+
117
+ if(t == "rem") {
118
+ removeNode(par, pos, html);
119
+ } else if(t == "add") {
120
+ addNode(par, pos, html);
121
+ } else if(t == "rep") {
122
+ replaceNode(par, pos, html);
123
+ }
124
+
125
+ addCSS(css);
126
+ addJS(js);
127
+ }
128
+
129
+ function addToHead(elt) {
130
+ var head = document.getElementsByTagName('head')[0];
131
+ head.appendChild(elt);
132
+ }
133
+
134
+ function addCSS(css) {
135
+ var style = document.createElement('style');
136
+ style.innerHTML = css;
137
+ addToHead(style);
138
+ }
139
+
140
+ function addJS(js) {
141
+ var script = document.createElement('script');
142
+ script.innerHTML = js;
143
+ addToHead(script);
144
+ Goat.loadComponents();
145
+ /*console.log("Eval " + js);
146
+ eval(js);*/
147
+ }
148
+
149
+ function updateReceived(m) {
150
+ var id = m.id;
151
+ var version = m.version;
152
+ var updates = m.updates;
153
+
154
+ if(m.version == rt.version + 1) {
155
+ $(updates).each(function(_,u) {
156
+ applyUpdate(u);
157
+ });
158
+ rt.version++;
159
+ } else if(m.version <= rt.version) {
160
+ // fine, we already have this
161
+ } else {
162
+ updateFailed("Tried to apply update " + version + ", but I'm only at version " + rt.version);
163
+ }
164
+ }
165
+
166
+ function showSpinner() {
167
+ $("#menu").prepend(GoatRT.spinner);
168
+ }
169
+
170
+ function hideSpinner() {
171
+ $("#spinner").remove();
172
+ }
173
+
174
+ function newTxn() {
175
+ var op = new RTTxn();
176
+ rt.ops[op.code] = op;
177
+ return op;
178
+ }
179
+
180
+ function beginTxn(txn) {
181
+ rt.pendingTxns[txn.code] = true;
182
+ }
183
+
184
+ function pendingCount() {
185
+ var pending = 0;
186
+ for(var k in rt.pendingTxns) pending++;
187
+ return pending;
188
+ }
189
+
190
+ function completeTxn(txn) {
191
+ if(rt.pendingTxns[txn]) {
192
+ delete rt.pendingTxns[txn];
193
+
194
+ if(pendingCount() == 0) {
195
+ if(!txn.start) hideSpinner();
196
+ if(txn.complete) txn.complete();
197
+ }
198
+ } else {
199
+ console.error("Can't complete txn " + txn + ": no such txn was started");
200
+ }
201
+ }
202
+
203
+ function txnCompleteMsg(msg) {
204
+ console.log('txnCompleteMsg');
205
+ console.log(msg);
206
+ completeTxn(msg['txn']);
207
+ }
208
+
209
+ rt.updateReceived = updateReceived;
210
+ rt.node = node;
211
+ rt.newTxn = newTxn;
212
+ rt.beginTxn = beginTxn;
213
+ rt.txnCompleteMsg = txnCompleteMsg;
214
+ rt.spinner = $("<img id=\"spinner\" style=\"position: relative; float: left; top: 50px; left: 20px;\" src=\"/static/img/spinner.gif\">");
215
+ rt.showSpinner = showSpinner;
216
+
217
+ return rt;
218
+ })(GoatRT);
219
+
220
+ var RTTxn = Class.extend({
221
+ init: function() {
222
+ this.code = String.random(8, 'txn');
223
+ },
224
+
225
+ onStart: function(s) {
226
+ this.start = s;
227
+ },
228
+
229
+ onComplete: function(c) {
230
+ this.complete = c;
231
+ }
232
+ });
233
+
234
+ var GoatRPC = Class.extend({
235
+ init: function(c, n) {
236
+ this.component = c;
237
+ this.name = n;
238
+ },
239
+
240
+ args: function(a) {
241
+ this.args = a;
242
+ return this;
243
+ },
244
+
245
+ data: function(d) {
246
+ this.data = d;
247
+ return this;
248
+ },
249
+
250
+ onRPCComplete: function(s) {
251
+ this.rpcComplete = s;
252
+ return this;
253
+ },
254
+
255
+ onRPCError: function(f) {
256
+ this.rpcError = f;
257
+ return this;
258
+ },
259
+
260
+ rpcSuccess: function(data) {
261
+ if(this.rpcComplete) this.rpcComplete(data, this);
262
+ },
263
+
264
+ rpcFailure: function() {
265
+ if(this.rpcError) this.rpcError(this);
266
+ },
267
+
268
+ onStart: function(start) {
269
+ this.start = start;
270
+ return this;
271
+ },
272
+
273
+ onUpdateComplete: function(c) {
274
+ this.updateComplete = c;
275
+ return this;
276
+ },
277
+
278
+ send: function() {
279
+ var data = this.data;
280
+
281
+ data['cls'] = this.component.cls;
282
+ data['id'] = this.component.id;
283
+ data['rpc'] = this.name;
284
+ data['pgid'] = Goat.page_id;
285
+ data['args'] = JSON.stringify(this.args);
286
+
287
+ if(this.rt) {
288
+ var txn = GoatRT.newTxn();
289
+ data['rttxn'] = txn.code;
290
+ txn.onComplete(this.updateComplete);
291
+ GoatRT.beginTxn(txn);
292
+ }
293
+
294
+ if(this.start) this.start()
295
+ else if(this.rt) GoatRT.showSpinner();
296
+
297
+ $.ajax({
298
+ type: 'POST',
299
+ url: '/rpc',
300
+ data: data,
301
+ success: closure(this, this.rpcSuccess),
302
+ error: closure(this, this.rpcFailure),
303
+ dataType: 'script',
304
+ async: true
305
+ });
306
+
307
+ return this;
308
+ },
309
+
310
+ rtUpdate: function() {
311
+ this.rt = true;
312
+ return this;
313
+ }
314
+ });
315
+
1
316
  var Goat = {
2
317
  channelOpenFails: 0,
3
318
  activeChannel: null,
319
+ pageDead: false,
4
320
  components: {},
5
321
  page_id: null
6
322
  }
@@ -14,33 +330,15 @@ function closure(target, fn) {
14
330
  $.extend(Goat, {
15
331
  closure: function(fn) { return closure(this, fn); },
16
332
 
17
- node: function(str) {
18
- return $('#' + str);
19
- },
20
-
21
- createNode: function(m) {
22
- var after = m['after'];
23
- var html = m['html'];
24
-
25
- if(after) {
26
- var anode = this.node(after);
27
- if(anode.size() == 0)
28
- console.error("Couldn't find node " + after + " to insert after");
29
- else
30
- anode.after(html);
31
- } else {
32
- $(document).after(html);
33
- }
333
+ channelURL: function() {
334
+ if(window.location.host.match(/127\./))
335
+ return 'http://127.0.0.1:8050/channel';
336
+ else
337
+ return '/channel';
34
338
  },
35
339
 
36
- removeNode: function(m) {
37
- var id = m['id'];
38
- var html = m['html'];
39
- var rm = this.node(id);
40
- if(rm.size() == 0)
41
- console.error("Couldn't find node " + id + " to remove");
42
- else
43
- rm.remove();
340
+ node: function(str) {
341
+ return $('#' + str);
44
342
  },
45
343
 
46
344
  showAlert: function(m) {
@@ -49,19 +347,20 @@ $.extend(Goat, {
49
347
  },
50
348
 
51
349
  messageReceived: function(m) {
350
+ console.log(m);
351
+
52
352
  var t = m['type'];
53
- if(t == 'update') {
54
- var id = m['id'];
55
- var html = m['html'];
56
- this.node(id).html(html);
353
+ if(t == 'component_updated') {
354
+ GoatRT.updateReceived(m);
57
355
  } else if(t == 'redirect') {
58
356
  console.log("Redirecting to " + m['location']);
59
357
  sleep(2);
60
358
  window.location = m['location'];
61
- } else if (t == 'create') {
62
- this.createNode(m);
63
- } else if (t == 'remove') {
64
- this.removeNode(m);
359
+ } else if(t == 'page_expired') {
360
+ alert("Due to inactivity, you'll need to refresh this page.");
361
+ Goat.pageDead = true;
362
+ } else if(t == 'txn_complete') {
363
+ GoatRT.txnCompleteMsg(m);
65
364
  } else if(t == 'alert') {
66
365
  this.showAlert(m);
67
366
  } else {
@@ -78,9 +377,6 @@ $.extend(Goat, {
78
377
 
79
378
  this.channelOpenFails = 0;
80
379
 
81
- if(this.messageReceivedDelegate)
82
- this.messageReceivedDelegate(data);
83
-
84
380
  if(data['messages']) {
85
381
  $(data['messages']).each(this.closure(function(i, m) { this.messageReceived(m) }));
86
382
  } else {
@@ -91,11 +387,16 @@ $.extend(Goat, {
91
387
  reopenChannel: function() {
92
388
  console.log("reopenChannel()");
93
389
  var fails = this.channelOpenFails;
94
- setTimeout(function() { Goat.openChannel(); }, Math.min(30000, Math.pow(1.5, fails)));
390
+ setTimeout(function() { Goat.openChannel(); }, Math.min(30000, Math.max(1000, Math.pow(1.5, fails))));
95
391
  this.channelOpenFails++;
96
392
  },
97
393
 
98
394
  openChannel: function() {
395
+ if(Goat.pageDead) {
396
+ console.log("not opening channel: page dead");
397
+ return;
398
+ }
399
+
99
400
  console.log("opening channel");
100
401
 
101
402
  if(this.activeChannel) {
@@ -122,8 +423,9 @@ $.extend(Goat, {
122
423
  var logError = closure(this, function() {
123
424
  console.error("Goat: error loading " + src);
124
425
  });
125
-
126
- var src = '/channel?_id=' + Goat.page_id + '&jsonp=' + jsonp;
426
+
427
+ var base = Goat.channelURL();
428
+ var src = base + '?_id=' + Goat.page_id + '&jsonp=' + jsonp + '&version=' + GoatRT.version;
127
429
 
128
430
  scriptTag.addEventListener('load', function() { cleanup(); Goat.reopenChannel(); }, false);
129
431
  scriptTag.addEventListener('error', function() { cleanup(); logError(); Goat.reopenChannel(); }, false);
@@ -157,88 +459,15 @@ $.extend(Goat, {
157
459
 
158
460
  return false;
159
461
  },
160
-
161
- remoteDispatch: function(c, k) {
162
- $.ajax({
163
- url: '/dispatch',
164
- async: true,
165
- data: {_id: Goat.page_id, _component: c, _dispatch: k},
166
- success: function(data) {},
167
- error: function(req, stat, err) {
168
- console.log("remote dispatch error");
169
- console.log({req: req, status: stat, err: err});
170
- }
171
- });
172
-
173
- return false;
174
- },
175
462
 
176
463
  wireComponent: function(id, obj) {
177
464
  this.components[id] = obj;
178
- }
179
- });
180
-
181
- (function(){
182
- var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
183
- // The base Class implementation (does nothing)
184
- this.Class = function(){};
185
-
186
- // Create a new Class that inherits from this class
187
- Class.extend = function(prop) {
188
- var _super = this.prototype;
189
-
190
- // Instantiate a base class (but only create the instance,
191
- // don't run the init constructor)
192
- initializing = true;
193
- var prototype = new this();
194
- initializing = false;
195
-
196
- // Copy the properties over onto the new prototype
197
- for (var name in prop) {
198
- // Check if we're overwriting an existing function
199
- prototype[name] = typeof prop[name] == "function" &&
200
- typeof _super[name] == "function" && fnTest.test(prop[name]) ?
201
- (function(name, fn){
202
- return function() {
203
- var tmp = this._super;
204
-
205
- // Add a new ._super() method that is the same method
206
- // but on the super-class
207
- this._super = _super[name];
208
-
209
- // The method only need to be bound temporarily, so we
210
- // remove it when we're done executing
211
- var ret = fn.apply(this, arguments);
212
- this._super = tmp;
213
-
214
- return ret;
215
- };
216
- })(name, prop[name]) :
217
- prop[name];
218
- }
219
-
220
- // The dummy class constructor
221
- function Class() {
222
- // All construction is actually done in the init method
223
- if ( !initializing && this.init )
224
- this.init.apply(this, arguments);
225
- }
226
-
227
- // Populate our constructed prototype object
228
- Class.prototype = prototype;
229
-
230
- // Enforce the constructor to be what we expect
231
- Class.constructor = Class;
232
-
233
- // And make this class extendable
234
- Class.extend = arguments.callee;
235
-
236
- return Class;
237
- };
238
- })();
465
+ },
239
466
 
240
- $(document).ready(function() {
241
- $.each(Goat.components, function(_, c) { c.onLoad(); });
242
- setTimeout(function() { if(Goat.page_id) Goat.openChannel(); }, 1000);
467
+ loadComponents: function() {
468
+ $.each(Goat.components, function(_, c) {
469
+ if(!c.isLoaded())
470
+ c.load();
471
+ });
472
+ }
243
473
  });
244
-