dripdrop 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/README.md +23 -28
  2. data/VERSION +1 -1
  3. data/dripdrop.gemspec +25 -3
  4. data/example/combined.rb +33 -0
  5. data/example/pubsub.rb +7 -15
  6. data/example/stats_app/core.rb +113 -0
  7. data/example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc +0 -0
  8. data/example/stats_app/public/backbone.js +16 -0
  9. data/example/stats_app/public/build_templates.rb +5 -0
  10. data/example/stats_app/public/json2.js +482 -0
  11. data/example/stats_app/public/protovis-r3.2.js +277 -0
  12. data/example/stats_app/public/stats.css +5 -0
  13. data/example/stats_app/public/stats.haml +61 -0
  14. data/example/stats_app/public/stats.html +26 -0
  15. data/example/stats_app/public/stats.js +113 -0
  16. data/example/stats_app/public/stats.scss +10 -0
  17. data/example/stats_app/public/underscore.js +17 -0
  18. data/example/xreq_xrep.rb +9 -11
  19. data/js/dripdrop.js +6 -2
  20. data/lib/dripdrop/handlers/base.rb +18 -0
  21. data/lib/dripdrop/handlers/http.rb +18 -18
  22. data/lib/dripdrop/handlers/websockets.rb +33 -26
  23. data/lib/dripdrop/handlers/zeromq.rb +30 -24
  24. data/lib/dripdrop/message.rb +5 -0
  25. data/lib/dripdrop/node/nodelet.rb +29 -0
  26. data/lib/dripdrop/node.rb +103 -25
  27. data/spec/gimite-websocket.rb +442 -0
  28. data/spec/message_spec.rb +5 -0
  29. data/spec/node/http_spec.rb +2 -8
  30. data/spec/node/nodelet_spec.rb +57 -0
  31. data/spec/node/routing_spec.rb +68 -0
  32. data/spec/node/websocket_spec.rb +88 -0
  33. data/spec/node/zmq_pushpull_spec.rb +2 -6
  34. data/spec/node/zmq_xrepxreq_spec.rb +24 -24
  35. data/spec/node_spec.rb +0 -1
  36. data/spec/spec_helper.rb +17 -3
  37. metadata +27 -5
  38. data/js/jack.js +0 -876
data/README.md CHANGED
@@ -4,48 +4,43 @@
4
4
 
5
5
  DripDrop is ZeroMQ(using zmqmachine) + Event Machine simplified for the general use case + serialization helpers.
6
6
 
7
- Here's an example of the kind of thing DripDrop makes easy, from [examples/pubsub.rb](http://github.com/andrewvc/dripdrop/blob/master/example/pubsub.rb)
8
-
9
- require 'dripdrop/node'
10
- Thread.abort_on_exception = true
11
-
12
- #Define our handlers
7
+ Here's an example of the kind of thing DripDrop makes easy, from [example/combined.rb](http://github.com/andrewvc/dripdrop/blob/master/example/combined.rb)
8
+
9
+ require 'dripdrop'
10
+ Thread.abort_on_exception = true #Always a good idea in multithreaded apps.
11
+
12
+ # Encapsulates our EM and ZMQ reactors
13
13
  DripDrop::Node.new do
14
- z_addr = 'tcp://127.0.0.1:2200'
14
+ # Define all our sockets
15
+ route :stats_pub, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
16
+ route :stats_sub1, :zmq_subscribe, stats_pub.address, :connect
17
+ route :stats_sub2, :zmq_subscribe, stats_pub.address, :connect
18
+ route :http_collector, :http_server, 'http://127.0.0.1:8080'
19
+ route :http_agent, :http_client, http_collector.address
15
20
 
16
- #Create a publisher
17
- pub = zmq_publish(z_addr,:bind)
18
-
19
- #Create two subscribers
20
- zmq_subscribe(z_addr,:connect).on_recv do |message|
21
- puts "Receiver 1 #{message.inspect}"
21
+ stats_sub1.on_recv do |message|
22
+ puts "Receiver 1: #{message.body}"
22
23
  end
23
- zmq_subscribe(z_addr, :connect).on_recv do |message|
24
- puts "Receiver 2 #{message.inspect}"
24
+ stats_sub2.on_recv do |message|
25
+ puts "Receiver 2: #{message.body}"
25
26
  end
26
27
 
27
- zm_reactor.periodical_timer(5) do
28
- #Sending a hash as a message implicitly transforms it into a DripDrop::Message
29
- pub.send_message(:name => 'test', :body => 'Test Payload')
30
- end
31
-
32
- http_server(addr).on_recv do |msg,response|
28
+ i = 0
29
+ http_collector.on_recv do |message,response|
33
30
  i += 1
34
- response.send_message(msg)
31
+ stats_pub.send_message(message)
32
+ response.send_message(:name => 'ack', :body => {:seq => i})
35
33
  end
36
34
 
37
35
  EM::PeriodicTimer.new(1) do
38
- client = http_client(addr)
39
36
  msg = DripDrop::Message.new('http/status', :body => "Success #{i}")
40
- client.send_message(msg) do |resp_msg|
41
- puts resp_msg.inspect
37
+ http_agent.send_message(msg) do |resp_msg|
38
+ puts "RESP: #{resp_msg.body['seq']}"
42
39
  end
43
40
  end
44
41
  end.start! #Start the reactor and block until complete
45
42
 
46
- Note that these aren't regular ZMQ sockets, and that the HTTP server isn't a regular server. They only speak and respond using DripDrop::Message formatted messages. For HTTP/WebSockets it's JSON that looks like {name: 'name', head: {}, body: anything}, for ZeroMQ it means BERT. There is a raw made that you can use for other message formats, but using DripDrop::Messages makes things easier, and for some socket types (like XREQ/XREP) the predefined format is very useful in matching requests to replies.
47
-
48
- Want to see a longer example encapsulating both zmqmachine and eventmachine functionality? Check out [this file](http://github.com/andrewvc/dripdrop-webstats/blob/master/lib/dripdrop-webstats.rb).
43
+ Note that these aren't regular ZMQ sockets, and that the HTTP server isn't a regular server. They only speak and respond using DripDrop::Message formatted messages. For HTTP/WebSockets it's JSON that looks like {name: 'name', head: {}, body: anything}, for ZeroMQ it means BERT. There is a raw mode that you can use for other message formats, but using DripDrop::Messages makes things easier, and for some socket types (like XREQ/XREP) the predefined format is very useful in matching requests to replies.
49
44
 
50
45
  #RDoc
51
46
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.4.0
data/dripdrop.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dripdrop}
8
- s.version = "0.3.1"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Andrew Cholakian"]
12
- s.date = %q{2010-10-21}
12
+ s.date = %q{2010-11-15}
13
13
  s.description = %q{Evented framework for ZeroMQ and EventMachine Apps. }
14
14
  s.email = %q{andrew@andrewvc.com}
15
15
  s.extra_rdoc_files = [
@@ -26,25 +26,43 @@ Gem::Specification.new do |s|
26
26
  "doc_img/topology.png",
27
27
  "dripdrop.gemspec",
28
28
  "example/agent_test.rb",
29
+ "example/combined.rb",
29
30
  "example/http.rb",
30
31
  "example/pubsub.rb",
31
32
  "example/pushpull.rb",
33
+ "example/stats_app/core.rb",
34
+ "example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc",
35
+ "example/stats_app/public/backbone.js",
36
+ "example/stats_app/public/build_templates.rb",
37
+ "example/stats_app/public/json2.js",
38
+ "example/stats_app/public/protovis-r3.2.js",
39
+ "example/stats_app/public/stats.css",
40
+ "example/stats_app/public/stats.haml",
41
+ "example/stats_app/public/stats.html",
42
+ "example/stats_app/public/stats.js",
43
+ "example/stats_app/public/stats.scss",
44
+ "example/stats_app/public/underscore.js",
32
45
  "example/subclass.rb",
33
46
  "example/xreq_xrep.rb",
34
47
  "js/dripdrop.html",
35
48
  "js/dripdrop.js",
36
- "js/jack.js",
37
49
  "js/qunit.css",
38
50
  "js/qunit.js",
39
51
  "lib/dripdrop.rb",
40
52
  "lib/dripdrop/agent.rb",
53
+ "lib/dripdrop/handlers/base.rb",
41
54
  "lib/dripdrop/handlers/http.rb",
42
55
  "lib/dripdrop/handlers/websockets.rb",
43
56
  "lib/dripdrop/handlers/zeromq.rb",
44
57
  "lib/dripdrop/message.rb",
45
58
  "lib/dripdrop/node.rb",
59
+ "lib/dripdrop/node/nodelet.rb",
60
+ "spec/gimite-websocket.rb",
46
61
  "spec/message_spec.rb",
47
62
  "spec/node/http_spec.rb",
63
+ "spec/node/nodelet_spec.rb",
64
+ "spec/node/routing_spec.rb",
65
+ "spec/node/websocket_spec.rb",
48
66
  "spec/node/zmq_pushpull_spec.rb",
49
67
  "spec/node/zmq_xrepxreq_spec.rb",
50
68
  "spec/node_spec.rb",
@@ -57,9 +75,13 @@ Gem::Specification.new do |s|
57
75
  s.summary = %q{Evented framework for ZeroMQ and EventMachine Apps.}
58
76
  s.test_files = [
59
77
  "spec/spec_helper.rb",
78
+ "spec/gimite-websocket.rb",
60
79
  "spec/node/http_spec.rb",
80
+ "spec/node/routing_spec.rb",
61
81
  "spec/node/zmq_xrepxreq_spec.rb",
62
82
  "spec/node/zmq_pushpull_spec.rb",
83
+ "spec/node/nodelet_spec.rb",
84
+ "spec/node/websocket_spec.rb",
63
85
  "spec/message_spec.rb",
64
86
  "spec/node_spec.rb"
65
87
  ]
@@ -0,0 +1,33 @@
1
+ require 'dripdrop'
2
+ Thread.abort_on_exception = true #Always a good idea in multithreaded apps.
3
+
4
+ # Encapsulates our EM and ZMQ reactors
5
+ DripDrop::Node.new do
6
+ # Define all our sockets
7
+ route :stats_pub, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
8
+ route :stats_sub1, :zmq_subscribe, stats_pub.address, :connect
9
+ route :stats_sub2, :zmq_subscribe, stats_pub.address, :connect
10
+ route :http_collector, :http_server, 'http://127.0.0.1:8080'
11
+ route :http_agent, :http_client, http_collector.address
12
+
13
+ stats_sub1.on_recv do |message|
14
+ puts "Receiver 1: #{message.body}"
15
+ end
16
+ stats_sub2.on_recv do |message|
17
+ puts "Receiver 2: #{message.body}"
18
+ end
19
+
20
+ i = 0
21
+ http_collector.on_recv do |message,response|
22
+ i += 1
23
+ stats_pub.send_message(message)
24
+ response.send_message(:name => 'ack', :body => {:seq => i})
25
+ end
26
+
27
+ EM::PeriodicTimer.new(1) do
28
+ msg = DripDrop::Message.new('http/status', :body => "Success #{i}")
29
+ http_agent.send_message(msg) do |resp_msg|
30
+ puts "RESP: #{resp_msg.body['seq']}"
31
+ end
32
+ end
33
+ end.start! #Start the reactor and block until complete
data/example/pubsub.rb CHANGED
@@ -3,33 +3,25 @@ Thread.abort_on_exception = true
3
3
 
4
4
  #Define our handlers
5
5
  DripDrop::Node.new do
6
- z_addr = 'tcp://127.0.0.1:2200'
6
+ route :pub, :zmq_publish, 'tcp://127.0.0.1:2200', :bind
7
+ route :sub1, :zmq_subscribe, pub.address, :connect, :topic_filter => /[13579]$/
8
+ route :sub2, :zmq_subscribe, pub.address, :connect, :topic_filter => /[02468]$/
9
+ route :sub3, :zmq_subscribe, pub.address, :connect
7
10
 
8
- #Create a publisher
9
- pub = zmq_publish(z_addr,:bind)
10
-
11
- #Create three subscribers
12
- sub1 = zmq_subscribe(z_addr,:connect)
13
-
14
11
  sub1.on_recv do |message|
15
12
  puts "Receiver 1 #{message.inspect}"
16
13
  end
17
14
 
18
- sub1.topic_filter = /[13579]$/
19
-
20
- sub2 = zmq_subscribe(z_addr,:connect)
21
-
22
15
  sub2.on_recv do |message|
23
16
  puts "Receiver 2 #{message.inspect}"
24
17
  end
25
18
 
26
- sub2.topic_filter = /[02468]$/
27
-
28
- zmq_subscribe(z_addr, :connect).on_recv do |message|
19
+ sub3.on_recv do |message|
29
20
  puts "Receiver 3 #{message.inspect}"
30
21
  end
31
22
 
32
- zm_reactor.periodical_timer(5) do
23
+ zm_reactor.periodical_timer(500) do
24
+ puts "Sending!"
33
25
  #Sending a hash as a message implicitly transforms it into a DripDrop::Message
34
26
  pub.send_message(:name => Time.now.to_i.to_s, :body => 'Test Payload')
35
27
  end
@@ -0,0 +1,113 @@
1
+ require 'dripdrop'
2
+ Thread.abort_on_exception = true #Always a good idea in multithreaded apps.
3
+
4
+ # This demo app is an message stats application
5
+ # It receives stats data via either HTTP or ZMQ directly, aggregates,
6
+ # and keeps track of data.
7
+ DripDrop::Node.new do
8
+ routes_for :agg do
9
+ route :input, :zmq_subscribe, 'tcp://127.0.0.1:2200', :bind
10
+ route :output, :zmq_publish, 'tcp://127.0.0.1:2201', :bind
11
+ route :input_http, :http_server, 'http://127.0.0.1:8082'
12
+ end
13
+
14
+ routes_for :counter do
15
+ route :input, :zmq_subscribe, agg_output.address, :connect
16
+ route :query, :zmq_xrep, 'tcp://127.0.0.1:2203', :bind
17
+ route :query_http, :http_server, 'tcp://0.0.0.0:8081'
18
+ end
19
+
20
+ routes_for :tracer do
21
+ route :input, :zmq_subscribe, agg_output.address, :connect, :topic_filter => /^ip_trace_req$/
22
+ route :output, :zmq_publish, 'tcp://127.0.0.1:2204', :bind
23
+ end
24
+
25
+ routes_for :ws_stream do
26
+ route :tracer_input, :zmq_subscribe, agg_output.address, :connect
27
+ route :agg_input, :zmq_subscribe, tracer_output.address, :connect
28
+ route :client, :websocket, 'ws://127.0.0.1:2202'
29
+ end
30
+
31
+ routes_for :heartbeat do
32
+ route :output, :zmq_publish, agg_input.address, :connect
33
+ end
34
+
35
+ nodelet :agg do |agg|
36
+ agg.input.on_recv do |message|
37
+ agg.output.send_message(message)
38
+ end
39
+
40
+ agg.input.on_recv do |message|
41
+ agg.output.send_message(message)
42
+ end
43
+
44
+ agg.input_http.on_recv do |message,response,env|
45
+ response.send_message(:name => 'ack')
46
+ agg.output.send_message(message)
47
+ end
48
+ end
49
+
50
+ nodelet :counter do |cntr|
51
+ stats = {:total => 0, :name_counts => Hash.new(0) }
52
+
53
+ cntr.input.on_recv do |message|
54
+ stats[:total] += 1
55
+ stats[:name_counts][message.name] += 1
56
+ end
57
+
58
+ cntr.query.on_recv do |message,ids,seq|
59
+ cntr.query.send_message({:name => 'stats', :body => @stats}, ids, seq)
60
+ end
61
+
62
+ cntr.query_http.on_recv do |message,response|
63
+ response.send_message(:name => 'stats', :body => @stats)
64
+ end
65
+ end
66
+
67
+ nodelet :tracer do |tracer|
68
+ tracer_memo = {}
69
+
70
+ ip_regexp = /\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\Z/
71
+ tracer.input.on_recv do |message|
72
+ puts "TRACE #{message.body.inspect}"
73
+
74
+ ip = message.body['ip']
75
+ puts "IP #{message.inspect}"
76
+ if ip =~ ip_regexp
77
+ memoized_res = tracer_memo[ip]
78
+ if memoized_res
79
+ tracer.output.send_message(:name => 'ip_route', :body => {:ip => ip, :route => memoized_res})
80
+ else
81
+ EM.system("/usr/sbin/traceroute -w 4 #{ip}") do |output,status|
82
+ route = output.split("\n")[1..-1].map {|l| l.split(/ /)[3] }.select {|a| a != '*'}
83
+ tracer_memo[ip] = route
84
+ tracer.output.send_message(:name => 'ip_route', :body => {:ip => ip, :route => route})
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ nodelet :ws_stream do |wss|
92
+ [wss.tracer_input, wss.agg_input].each do |input|
93
+ input.on_recv do |message|
94
+ send_internal(:wss, message)
95
+ end
96
+ end
97
+
98
+ wss.client.on_open do |ws|
99
+ recv_internal(:wss, ws.signature) do |message|
100
+ ws.send_message(message)
101
+ end
102
+ end.on_recv do |message,ws|
103
+ end.on_close do |ws|
104
+ end.on_error do |ws|
105
+ end
106
+ end
107
+
108
+ nodelet :heartbeat do |hbeat|
109
+ zm_reactor.periodical_timer(1000) do
110
+ hbeat.output.send_message(:name => 'heartbeat/tick', :body => Time.now.to_i)
111
+ end
112
+ end
113
+ end.start! #Start the reactor and block until complete
@@ -0,0 +1,16 @@
1
+ (function(){var f;f=typeof exports!=="undefined"?exports:this.Backbone={};f.VERSION="0.2.0";var e=this._;if(!e&&typeof require!=="undefined")e=require("underscore")._;var h=this.jQuery;f.emulateHttp=false;f.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d<g;d++)if(b===c[d]){c.splice(d,1);break}}else c[a]=[]}else this._callbacks=
2
+ {};return this},trigger:function(a){var b,c,d,g;if(!(c=this._callbacks))return this;if(b=c[a]){d=0;for(g=b.length;d<g;d++)b[d].apply(this,Array.prototype.slice.call(arguments,1))}if(b=c.all){d=0;for(g=b.length;d<g;d++)b[d].apply(this,arguments)}return this}};f.Model=function(a){this.attributes={};this.cid=e.uniqueId("c");this.set(a||{},{silent:true});this._previousAttributes=e.clone(this.attributes);this.initialize&&this.initialize(a)};e.extend(f.Model.prototype,f.Events,{_previousAttributes:null,
3
+ _changed:false,toJSON:function(){return e.clone(this.attributes)},get:function(a){return this.attributes[a]},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes;if(this.validate){var d=this.validate(a);if(d){b.error?b.error(this,d):this.trigger("error",this,d);return false}}if("id"in a)this.id=a.id;for(var g in a){d=a[g];if(d==="")d=null;if(!e.isEqual(c[g],d)){c[g]=d;if(!b.silent){this._changed=true;this.trigger("change:"+g,this,d)}}}!b.silent&&this._changed&&
4
+ this.change();return this},unset:function(a,b){b||(b={});var c=this.attributes[a];delete this.attributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this);this.change()}return c},fetch:function(a){a||(a={});var b=this,c=a.error&&e.bind(a.error,null,b);f.sync("read",this,function(d){if(!b.set(b.parse(d),a))return false;a.success&&a.success(b,d)},c);return this},save:function(a,b){a||(a={});b||(b={});if(!this.set(a,b))return false;var c=this,d=b.error&&e.bind(b.error,null,c),g=this.isNew()?
5
+ "create":"update";f.sync(g,this,function(i){if(!c.set(c.parse(i),b))return false;b.success&&b.success(c,i)},d);return this},destroy:function(a){a||(a={});var b=this,c=a.error&&e.bind(a.error,null,b);f.sync("delete",this,function(d){b.collection&&b.collection.remove(b);a.success&&a.success(b,d)},c);return this},url:function(){var a=j(this.collection);if(this.isNew())return a;return a+"/"+this.id},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},
6
+ change:function(){this.trigger("change",this);this._previousAttributes=e.clone(this.attributes);this._changed=false},hasChanged:function(a){if(a)return this._previousAttributes[a]!=this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=false,d;for(d in a)if(!e.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return e.clone(this._previousAttributes)}});
7
+ f.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=e.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize&&this.initialize(a,b)};e.extend(f.Collection.prototype,f.Events,{model:f.Model,toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(e.isArray(a))for(var c=0,d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,
8
+ b){if(e.isArray(a))for(var c=0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){return a&&this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("refresh",this);return this},pluck:function(a){return e.map(this.models,function(b){return b.get(a)})},
9
+ refresh:function(a,b){a||(a=[]);b||(b={});this._reset();this.add(a,{silent:true});b.silent||this.trigger("refresh",this);return this},fetch:function(a){a||(a={});var b=this,c=a.error&&e.bind(a.error,null,b);f.sync("read",this,function(d){b.refresh(b.parse(d));a.success&&a.success(b,d)},c);return this},create:function(a,b){b||(b={});a instanceof f.Model||(a=new this.model(a));var c=a.collection=this;return a.save(null,{success:function(d,g){c.add(d);b.success&&b.success(d,g)},error:b.error})},parse:function(a){return a},
10
+ chain:function(){return e(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_add:function(a,b){b||(b={});a instanceof f.Model||(a=new this.model(a));var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;a.collection=this;this.models.splice(this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._boundOnModelEvent);this.length++;b.silent||this.trigger("add",
11
+ a);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];delete a.collection;this.models.splice(this.indexOf(a),1);a.unbind("all",this._boundOnModelEvent);this.length--;b.silent||this.trigger("remove",a);return a},_onModelEvent:function(a,b){if(a==="change:id"){delete this._byId[b.previous("id")];this._byId[b.id]=b}this.trigger.apply(this,arguments)}});e.each(["forEach","each","map","reduce","reduceRight","find","detect","filter",
12
+ "select","reject","every","all","some","any","include","invoke","max","min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty"],function(a){f.Collection.prototype[a]=function(){return e[a].apply(e,[this.models].concat(e.toArray(arguments)))}});f.View=function(a){this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize&&this.initialize(a)};var k=function(a){return h(a,this.el)},l=/^(\w+)\s*(.*)$/;e.extend(f.View.prototype,
13
+ {tagName:"div",$:k,jQuery:k,render:function(){return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(!(a||(a=this.events)))return this;h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(l),g=d[1];d=d[2];c=e.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}return this},_configure:function(a){if(this.options)a=e.extend({},this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=
14
+ a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a.className=this.className;this.el=this.make(this.tagName,a)}}});var n=f.Model.extend=f.Collection.extend=f.View.extend=function(a,b){var c=m(this,a,b);c.extend=n;return c},o={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};f.sync=function(a,b,c,d){var g=
15
+ a==="create"||a==="update"?{model:JSON.stringify(b)}:{};a=o[a];if(f.emulateHttp&&(a==="PUT"||a==="DELETE")){g._method=a;a="POST"}h.ajax({url:j(b),type:a,data:g,dataType:"json",success:c,error:d})};var m=function(a,b,c){var d;d=b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};var g=function(){};g.prototype=a.prototype;d.prototype=new g;e.extend(d.prototype,b);c&&e.extend(d,c);return d.prototype.constructor=d},j=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified");
16
+ return e.isFunction(a.url)?a.url():a.url}})();
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ #Compile the HAML and SASS templates
3
+
4
+ `haml stats.haml > stats.html`
5
+ `sass stats.scss > stats.css`