dripdrop 0.3.1 → 0.4.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.
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`