dripdrop 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +23 -28
- data/VERSION +1 -1
- data/dripdrop.gemspec +25 -3
- data/example/combined.rb +33 -0
- data/example/pubsub.rb +7 -15
- data/example/stats_app/core.rb +113 -0
- data/example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc +0 -0
- data/example/stats_app/public/backbone.js +16 -0
- data/example/stats_app/public/build_templates.rb +5 -0
- data/example/stats_app/public/json2.js +482 -0
- data/example/stats_app/public/protovis-r3.2.js +277 -0
- data/example/stats_app/public/stats.css +5 -0
- data/example/stats_app/public/stats.haml +61 -0
- data/example/stats_app/public/stats.html +26 -0
- data/example/stats_app/public/stats.js +113 -0
- data/example/stats_app/public/stats.scss +10 -0
- data/example/stats_app/public/underscore.js +17 -0
- data/example/xreq_xrep.rb +9 -11
- data/js/dripdrop.js +6 -2
- data/lib/dripdrop/handlers/base.rb +18 -0
- data/lib/dripdrop/handlers/http.rb +18 -18
- data/lib/dripdrop/handlers/websockets.rb +33 -26
- data/lib/dripdrop/handlers/zeromq.rb +30 -24
- data/lib/dripdrop/message.rb +5 -0
- data/lib/dripdrop/node/nodelet.rb +29 -0
- data/lib/dripdrop/node.rb +103 -25
- data/spec/gimite-websocket.rb +442 -0
- data/spec/message_spec.rb +5 -0
- data/spec/node/http_spec.rb +2 -8
- data/spec/node/nodelet_spec.rb +57 -0
- data/spec/node/routing_spec.rb +68 -0
- data/spec/node/websocket_spec.rb +88 -0
- data/spec/node/zmq_pushpull_spec.rb +2 -6
- data/spec/node/zmq_xrepxreq_spec.rb +24 -24
- data/spec/node_spec.rb +0 -1
- data/spec/spec_helper.rb +17 -3
- metadata +27 -5
- 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 [
|
8
|
-
|
9
|
-
require 'dripdrop
|
10
|
-
Thread.abort_on_exception = true
|
11
|
-
|
12
|
-
#
|
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
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
24
|
-
puts "Receiver 2 #{message.
|
24
|
+
stats_sub2.on_recv do |message|
|
25
|
+
puts "Receiver 2: #{message.body}"
|
25
26
|
end
|
26
27
|
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
41
|
-
puts resp_msg.
|
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
|
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.
|
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.
|
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-
|
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
|
]
|
data/example/combined.rb
ADDED
@@ -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
|
-
|
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
|
-
|
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(
|
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
|
data/example/stats_app/public/.sass-cache/b48b4299d80c05f528daf63fe51d85e5e3c10d98/stats.scssc
ADDED
Binary file
|
@@ -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}})();
|