├── .gitignore
├── LICENSE
├── Procfile
├── README.md
├── Rakefile
├── client.js
├── client
├── .document
├── .gitignore
├── README
├── Rakefile
├── VERSION
├── examples
│ ├── juggernaut_observer.js
│ ├── juggernaut_observer.rb
│ └── roster.rb
├── juggernaut.gemspec
├── lib
│ ├── juggernaut.rb
│ └── juggernaut
│ │ └── rails
│ │ └── engine.rb
└── vendor
│ └── assets
│ └── javascripts
│ ├── json.js
│ ├── juggernaut.js
│ └── socket_io.js
├── index.js
├── lib
└── juggernaut
│ ├── channel.js
│ ├── client.js
│ ├── connection.js
│ ├── events.js
│ ├── ext
│ └── array.js
│ ├── index.js
│ ├── klass.js
│ ├── message.js
│ ├── publish.js
│ ├── redis.js
│ └── server.js
├── package.json
├── public
├── application.js
└── index.html
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | daemon.node/
2 | lib/daemon.node
3 | keys
4 | node_modules
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010 Alexander MacCaw
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node server.js
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Juggernaut
2 |
3 | ## Juggernaut has been **deprecated**! Read why [here](http://blog.alexmaccaw.com/killing-a-library).
4 |
5 | Juggernaut gives you a realtime connection between your servers and client browsers.
6 | You can literally push data to clients using your web application, which lets you do awesome things like multiplayer gaming, chat, group collaboration and more.
7 |
8 | Juggernaut is built on top of [Node.js](http://nodejs.org) and is super simple and easy to get going.
9 |
10 | ##Features
11 |
12 | * [Node.js](http://nodejs.org) server
13 | * Ruby client
14 | * Supports the following protocols:
15 | * WebSocket
16 | * Adobe Flash Socket
17 | * ActiveX HTMLFile (IE)
18 | * Server-Sent Events (Opera)
19 | * XHR with multipart encoding
20 | * XHR with long-polling
21 | * Horizontal scaling
22 | * Reconnection support
23 | * SSL support
24 |
25 | As you can see, Juggernaut supports a variety of protocols. If one isn't supported by a client, Juggernaut will fallback to one that is.
26 |
27 | Supported browsers are:
28 |
29 | * Desktop
30 | * Internet Explorer >= 5.5
31 | * Safari >= 3
32 | * Google Chrome >= 4
33 | * Firefox >= 3
34 | * Opera 10.61
35 | * Mobile
36 | * iPhone Safari
37 | * iPad Safari
38 | * Android WebKit
39 | * WebOs WebKit
40 |
41 | ##Requirements
42 |
43 | * Node.js
44 | * Redis
45 | * Ruby (optional)
46 |
47 | ##Setup
48 |
49 | ###Install [Node.js](http://nodejs.org)
50 |
51 | If you're using the [Brew](http://mxcl.github.com/homebrew) package management system, use that:
52 |
53 | brew install node
54 |
55 | Or follow the [Node build instructions](https://github.com/joyent/node/wiki/Installation)
56 |
57 | ###Install [Redis](http://code.google.com/p/redis)
58 |
59 | If you're using the Brew package, use that:
60 |
61 | brew install redis
62 |
63 | Or follow the [Redis build instructions](http://redis.io/download)
64 |
65 | ###Install Juggernaut
66 |
67 | Juggernaut is distributed by [npm](http://npmjs.org), you'll need to [install that](http://npmjs.org) first if you haven't already.
68 |
69 | npm install -g juggernaut
70 |
71 | ###Install the [Juggernaut client gem](http://rubygems.org/gems/juggernaut)
72 |
73 | This step is optional, but if you're planning on using Juggernaut with Ruby, you'll need the gem.
74 |
75 | gem install juggernaut
76 |
77 | ##Running
78 |
79 | Start Redis:
80 |
81 | redis-server
82 |
83 | Start Juggernaut:
84 |
85 | juggernaut
86 |
87 | That's it! Now go to [http://localhost:8080](http://localhost:8080) to see Juggernaut in action.
88 |
89 | ##Basic usage
90 |
91 | Everything in Juggernaut is done within the context of a channel. JavaScript clients can subscribe to a channel which your server can publish to.
92 | First, we need to include Juggernaut's application.js file. By default, Juggernaut is hosted on port 8080 - so we can just link to the file there.
93 |
94 |
95 |
96 | We then need to instantiate the Juggernaut object and subscribe to the channel. As you can see, subscribe takes two arguments, the channel name and a callback.
97 |
98 |
104 |
105 | That's it for the client side. Now, to publish to the channel we'll write some Ruby:
106 |
107 | require "juggernaut"
108 | Juggernaut.publish("channel1", "Some data")
109 |
110 | You should see the data we sent appear instantly in the [open browser window](http://localhost:8080).
111 | As well as strings, we can even pass objects, like so:
112 |
113 | Juggernaut.publish("channel1", {:some => "data"})
114 |
115 | The publish method also takes an array of channels, in case you want to send a message to multiple channels co-currently.
116 |
117 | Juggernaut.publish(["channel1", "channel2"], ["foo", "bar"])
118 |
119 | That's pretty much the gist of it, the two methods - publish and subscribe. Couldn't be easier than that!
120 |
121 | ##Flash
122 |
123 | Adobe Flash is optional, but it's the default fallback for a lot of browsers until WebSockets are supported.
124 | However, Flash needs a XML policy file to be served from port 843, which is restricted. You'll need to run Juggernaut with root privileges in order to open that port.
125 |
126 | sudo juggernaut
127 |
128 | You'll also need to specify the location of WebSocketMain.swf. Either copy this file (from Juggernaut's public directory) to the root public directory of your application, or specify it's location before instantiating Juggernaut:
129 |
130 | window.WEB_SOCKET_SWF_LOCATION = "http://juggaddress:8080/WebSocketMain.swf"
131 |
132 | As I mentioned above, using Flash with Juggernaut is optional - you don't have to run the server with root privileges. If Flash isn't available, Juggernaut will use [WebSockets](http://en.wikipedia.org/wiki/WebSocket) (the default), [Comet](http://goo.gl/lO6S) or polling.
133 |
134 | ##SSL
135 |
136 | Juggernaut has SSL support! To activate, just put create a folder called 'keys' in the 'juggernaut' directory,
137 | containing your privatekey.pem and certificate.pem files.
138 |
139 | >> mkdir keys
140 | >> cd keys
141 | >> openssl genrsa -out privatekey.pem 1024
142 | >> openssl req -new -key privatekey.pem -out certrequest.csr
143 | >> openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem
144 |
145 | Then, pass the secure option when instantiating Juggernaut in JavaScript:
146 |
147 | var juggernaut = new Juggernaut({secure: true})
148 |
149 | All Juggernaut's communication will now be encrypted by SSL.
150 |
151 | ##Scaling
152 |
153 | The only centralised (i.e. potential bottle neck) part to Juggernaut is Redis.
154 | Redis can support hundreds of thousands writes a second, so it's unlikely that will be an issue.
155 |
156 | Scaling is just a case of starting up more Juggernaut Node servers, all sharing the same Redis instance.
157 | Put a TCP load balancer in front them, distribute clients with a Round Robin approach, and use sticky sessions.
158 |
159 | It's worth noting that the latest WebSocket specification breaks support for a lot of HTTP load balancers, so it's safer just using a TCP one.
160 |
161 | ##Client Events
162 |
163 | Juggernaut's JavaScript client has a few events that you can bind to:
164 |
165 | * connect
166 | * disconnect
167 | * reconnect
168 |
169 | Juggernaut also triggers data events in the context of an channel. You can bind to that event by just passing a callback to the subscribe function.
170 | Here's an example of event binding. We're using [jQuery UI](http://jqueryui.com) to show a popup when the client loses their connection to our server.
171 |
172 | var jug = new Juggernaut;
173 |
174 | var offline = $("
")
175 | .html("The connection has been disconnected!
" +
176 | "Please go back online to use this service.")
177 | .dialog({
178 | autoOpen: false,
179 | modal: true,
180 | width: 330,
181 | resizable: false,
182 | closeOnEscape: false,
183 | title: "Connection"
184 | });
185 |
186 | jug.on("connect", function(){
187 | offline.dialog("close");
188 | });
189 |
190 | jug.on("disconnect", function(){
191 | offline.dialog("open");
192 | });
193 |
194 | // Once we call subscribe, Juggernaut tries to connnect.
195 | jug.subscribe("channel1", function(data){
196 | console.log("Got data: " + data);
197 | });
198 |
199 | ##Excluding certain clients
200 |
201 | It's a common use case to send messages to every client, except one. For example, this is a common chat scenario:
202 |
203 | * User creates chat message
204 | * User's client appends the message to the chat log, so the user sees it instantly
205 | * User's client sends an AJAX request to the server, notifying it of the new chat message
206 | * The server then publishes the chat message to all relevant clients
207 |
208 | Now, the issue above is if the server publishes the chat message back to the original client. In which case, it would get duplicated in the chat logs (as it already been created). We can resolve this issue by recording the client's Juggernaut ID, and then passing it as an `:except` option when Juggernaut publishes.
209 |
210 | You can pass the Juggernaut session ID along with any AJAX requests by hooking into `beforeSend`, which is triggered by jQuery before sending any AJAX requests. The callback is passed an XMLHttpRequest, which we can use to set a custom header specifying the session ID.
211 |
212 | var jug = new Juggernaut;
213 |
214 | jQuery.beforeSend(function(xhr){
215 | xhr.setRequestHeader("X-Session-ID", jug.sessionID);
216 | });
217 |
218 | Now, when we publish to a channel, we can pass the `:except` option, with the current client's session ID.
219 |
220 | Juggernaut.publish(
221 | "/chat",
222 | params[:body],
223 | :except => request.headers["X-Session-ID"]
224 | )
225 |
226 | Now, the original client won't get the duplicated chat message, even if it's subscribed to the __/chat__ channel.
227 |
228 | ##Server Events
229 |
230 | When a client connects & disconnects, Juggernaut triggers a callback. You can listen to these callbacks from the Ruby client,
231 |
232 | Juggernaut.subscribe do |event, data|
233 | # Use event/data
234 | end
235 |
236 | The event is either `:subscribe` or `:unsubscribe`. The data variable is just a hash of the client details:
237 |
238 | {"channel" => "channel1", "session_id" => "1822913980577141", "meta" => "foo"}
239 |
240 | ##Metadata
241 |
242 | You'll notice there's a meta attribute in the server event example above. Juggernaut lets you attach meta data to the client object,
243 | which gets passed along to any server events. For example, you could set User ID meta data - then you would know which user was subscribing/unsubscribing to channels. You could use this information to build a live Roster of online users.
244 |
245 | var jug = new Juggernaut;
246 | jug.meta = {user_id: 1};
247 |
248 | ##Using Juggernaut from Python
249 |
250 | You don't have to use Ruby to communicate with Juggernaut. In fact, all that is needed is a [Redis](http://code.google.com/p/redis) adapter. Here we're using [Python](http://www.python.org) with [redis-py](http://github.com/andymccurdy/redis-py).
251 |
252 | import redis
253 | import json
254 |
255 | msg = {
256 | "channels": ["channel1"],
257 | "data": "foo"
258 | }
259 |
260 | r = redis.Redis()
261 | r.publish("juggernaut", json.dumps(msg))
262 |
263 | ##Using Juggernaut from Node.js
264 |
265 | Similar to the Python example, we can use a Node.js Redis adapter to publish to Juggernaut.
266 |
267 | var redis = require("redis");
268 |
269 | var msg = {
270 | "channels": ["channel1"],
271 | "data": "foo"
272 | };
273 |
274 | var client = redis.createClient();
275 | client.publish("juggernaut", JSON.stringify(msg));
276 |
277 | ##Building a Roster
278 |
279 | So, let's take all we've learnt about Juggernaut, and apply it to something practical - a live chat roster.
280 | Here's the basic class. We're using [SuperModel](http://github.com/maccman/supermodel) with the Redis adapter. Any changes to the model will be saved to our Redis data store. We're also associating each Roster record with a user.
281 |
282 | class Roster < SuperModel::Base
283 | include SuperModel::Redis::Model
284 | include SuperModel::Timestamp::Model
285 |
286 | belongs_to :user
287 | validates_presence_of :user_id
288 |
289 | indexes :user_id
290 | end
291 |
292 | Now let's integrate the Roster class with Juggernaut. We're going to listen to Juggernaut's server events - fetching the user_id out of the events meta data, and calling __event_subscribe__ or __event_unsubscribe__, depending on the event type.
293 |
294 | def self.subscribe
295 | Juggernaut.subscribe do |event, data|
296 | user_id = data["meta"] && data["meta"]["user_id"]
297 | next unless user_id
298 |
299 | case event
300 | when :subscribe
301 | event_subscribe(user_id)
302 | when :unsubscribe
303 | event_unsubscribe(user_id)
304 | end
305 | end
306 | end
307 |
308 | Let's implement those two methods __event_subscribe__ & __event_unsubscribe__. We need to take into account they may be called multiple times for a particular user_id, if a User opens multiple browser windows co-currently.
309 |
310 | def event_subscribe(user_id)
311 | record = find_by_user_id(user_id) || self.new(:user_id => user_id)
312 | record.increment!
313 | end
314 |
315 | def event_unsubscribe(user_id)
316 | record = find_by_user_id(user_id)
317 | record && record.decrement!
318 | end
319 |
320 | We need to add a __count__ attribute to the Roster class, so we can track if a client has completely disconnected from the system.
321 | Whenever clients subscribes to a channel, __increment!__ will get called and the __count__ attribute will be incremented, conversly whenever they disconnect from that channel __decrement!__ will get called and __count__ decremented.
322 |
323 | attributes :count
324 |
325 | def count
326 | read_attribute(:count) || 0
327 | end
328 |
329 | def increment!
330 | self.count += 1
331 | save!
332 | end
333 |
334 | def decrement!
335 | self.count -= 1
336 | self.count > 0 ? save! : destroy
337 | end
338 |
339 | When __decrement!__ is called, we check to see if the count is zero, i.e. a client is no longer connected, and destroy the record if necessary. Now, at this point we have a live list of Roster records indicating who's online. We just need to call __Roster.subscribe__, say in a Rails script file, and Juggernaut events will be processed.
340 |
341 | #!/usr/bin/env ruby
342 | require File.expand_path('../../config/environment', __FILE__)
343 |
344 | puts "Starting Roster"
345 | Roster.subscribe
346 |
347 | There's no point, however, in having a live Roster unless we can show that to users - which is the subject of the next section, observing models.
348 |
349 | ##Observing models
350 |
351 | We can create an Juggernaut observer, which will observe some of the models, notifying clients when they're changed.
352 |
353 | class JuggernautObserver < ActiveModel::Observer
354 | observe :roster
355 |
356 | def after_create(rec)
357 | publish(:create, rec)
358 | end
359 |
360 | def after_update(rec)
361 | publish(:update, rec)
362 | end
363 |
364 | def after_destroy(rec)
365 | publish(:destroy, rec)
366 | end
367 |
368 | protected
369 | def publish(type, rec)
370 | channels = Array(rec.observer_clients).map {|c| "/observer/#{c}" }
371 | Juggernaut.publish(
372 | channels,
373 | {
374 | :id => rec.id,
375 | :type => type,
376 | :klass => rec.class.name,
377 | :record => rec
378 | }
379 | )
380 | end
381 | end
382 |
383 | So, you can see we're calling the publish method whenever a record is created/updated/destroyed. You'll notice that we're calling __observer_clients__ on the updated record. This is a method that application specific, and needs to be implemented on the Roster class. It needs to return an array of user_ids associated with the record.
384 |
385 | So, as to the JavaScript side to the observer, we need to subscribe to a observer channel and set a callback. Now, whenever a __Roster__ record is created/destroyed, the process function will be called. We can then update the UI accordingly.
386 |
387 | var process = function(msg){
388 | // msg.klass
389 | // msg.type
390 | // msg.id
391 | // msg.record
392 | };
393 |
394 | var jug = new Juggernaut;
395 | jug.subscribe("/observer/" + user_id, process);
396 |
397 | ##Full examples
398 |
399 | You can see the full examples inside [Holla](http://github.com/maccman/holla), specifically [roster.rb](https://github.com/maccman/holla/blob/original/app/models/roster.rb), [juggernaut_observer.rb](https://github.com/maccman/holla/blob/original/app/observers/juggernaut_observer.rb) and [application.juggernaut.js](https://github.com/maccman/holla/blob/original/app/javascripts/application.juggernaut.js).
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "tempfile"
2 | require "yui/compressor"
3 | require "fileutils"
4 | require "sprockets"
5 |
6 | APP_PATH = File.expand_path("./public/application.js")
7 | CLIENT_PATH = File.expand_path("./client.js")
8 |
9 | task :build do
10 | env = Sprockets::Environment.new
11 | env.append_path 'client/vendor/assets/javascripts'
12 | File.open(APP_PATH, 'w') { |f| f << env['juggernaut.js'].to_s }
13 | File.open(CLIENT_PATH, 'w') { |f| f << env['juggernaut.js'].to_s }
14 | end
15 |
16 | task :compress do
17 | tempfile = Tempfile.new("yui")
18 | compressor = YUI::JavaScriptCompressor.new(:munge => true)
19 | File.open(APP_PATH, "r") do |file|
20 | compressor.compress(file) do |compressed|
21 | while buffer = compressed.read(4096)
22 | tempfile.write(buffer)
23 | end
24 | end
25 | end
26 |
27 | tempfile.close
28 | FileUtils.mv(tempfile.path, APP_PATH)
29 | end
30 |
31 | task :default => [:build, :compress]
32 |
--------------------------------------------------------------------------------
/client/.document:
--------------------------------------------------------------------------------
1 | README.rdoc
2 | lib/**/*.rb
3 | bin/*
4 | features/**/*.feature
5 | LICENSE
6 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | ## MAC OS
2 | .DS_Store
3 |
4 | ## TEXTMATE
5 | *.tmproj
6 | tmtags
7 |
8 | ## EMACS
9 | *~
10 | \#*
11 | .\#*
12 |
13 | ## VIM
14 | *.swp
15 |
16 | ## PROJECT::GENERAL
17 | coverage
18 | rdoc
19 | pkg
20 |
21 | ## PROJECT::SPECIFIC
22 |
--------------------------------------------------------------------------------
/client/README:
--------------------------------------------------------------------------------
1 | = Juggernaut
2 |
3 | See http://github.com/maccman/juggernaut
4 |
5 | == Copyright
6 |
7 | Copyright (c) 2010 Alex MacCaw.
8 | See http://github.com/maccman/juggernaut/blob/master/LICENSE for details.
9 |
--------------------------------------------------------------------------------
/client/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 |
4 | begin
5 | require 'jeweler'
6 | Jeweler::Tasks.new do |gem|
7 | gem.name = "juggernaut"
8 | gem.summary = %Q{Simple realtime push}
9 | gem.description = %Q{Use Juggernaut to easily implement realtime chat, collaboration, gaming and much more!}
10 | gem.email = "info@eribium.org"
11 | gem.homepage = "http://github.com/maccman/juggernaut"
12 | gem.authors = ["Alex MacCaw"]
13 | gem.add_dependency "redis", ">= 0"
14 | end
15 | Jeweler::GemcutterTasks.new
16 | rescue LoadError
17 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18 | end
--------------------------------------------------------------------------------
/client/VERSION:
--------------------------------------------------------------------------------
1 | 2.1.1
--------------------------------------------------------------------------------
/client/examples/juggernaut_observer.js:
--------------------------------------------------------------------------------
1 | // Assumes you're using SuperModel
2 | // http://github.com/maccman/supermodel-js
3 |
4 | jQuery(function($){
5 | var jug = new Juggernaut;
6 | jug.subscribe("/sync/your_user_id", function(sync){
7 | var klass = eval(sync.klass);
8 | switch(sync.type) {
9 | case "create":
10 | klass.create(sync.record);
11 | break;
12 | case "update":
13 | klass.update(sync.id, sync.record);
14 | break;
15 | case "destroy":
16 | klass.destroy(sync.id);
17 | break;
18 | default:
19 | throw("Unknown type:" + type);
20 | }
21 | });
22 | })
--------------------------------------------------------------------------------
/client/examples/juggernaut_observer.rb:
--------------------------------------------------------------------------------
1 | class JuggernautObserver < ActiveRecord::Observer
2 | observe :activity, :user
3 |
4 | def after_create(rec)
5 | publish(:create, rec)
6 | end
7 |
8 | def after_update(rec)
9 | publish(:update, rec)
10 | end
11 |
12 | def after_destroy(rec)
13 | publish(:destroy, rec)
14 | end
15 |
16 | protected
17 | def publish(type, rec)
18 | Juggernaut.publish(
19 | Array(rec.sync_clients).map {|c| "/sync/#{c}" },
20 | {:type => type, :id => rec.id,
21 | :klass => rec.class.name, :record => rec}
22 | )
23 | end
24 | end
--------------------------------------------------------------------------------
/client/examples/roster.rb:
--------------------------------------------------------------------------------
1 | clients = {}
2 |
3 | Juggernaut.subscribe do |event, data|
4 | case event
5 | when :subscribe
6 | clients[data.session_id] = data
7 | when :unsubscribe
8 | clients.delete(data.session_id)
9 | end
10 | end
--------------------------------------------------------------------------------
/client/juggernaut.gemspec:
--------------------------------------------------------------------------------
1 | # Generated by jeweler
2 | # DO NOT EDIT THIS FILE DIRECTLY
3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4 | # -*- encoding: utf-8 -*-
5 |
6 | Gem::Specification.new do |s|
7 | s.name = %q{juggernaut}
8 | s.version = "2.1.1"
9 |
10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11 | s.authors = [%q{Alex MacCaw}]
12 | s.date = %q{2012-01-03}
13 | s.description = %q{Use Juggernaut to easily implement realtime chat, collaboration, gaming and much more!}
14 | s.email = %q{info@eribium.org}
15 | s.files = [
16 | ".document",
17 | ".gitignore",
18 | "README",
19 | "Rakefile",
20 | "VERSION",
21 | "examples/juggernaut_observer.js",
22 | "examples/juggernaut_observer.rb",
23 | "examples/roster.rb",
24 | "juggernaut.gemspec",
25 | "lib/juggernaut.rb",
26 | "lib/juggernaut/rails/engine.rb",
27 | "vendor/assets/javascripts/json.js",
28 | "vendor/assets/javascripts/juggernaut.js",
29 | "vendor/assets/javascripts/socket_io.js"
30 | ]
31 | s.homepage = %q{http://github.com/maccman/juggernaut}
32 | s.require_paths = [%q{lib}]
33 | s.rubygems_version = %q{1.8.6}
34 | s.summary = %q{Simple realtime push}
35 |
36 | if s.respond_to? :specification_version then
37 | s.specification_version = 3
38 |
39 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
40 | s.add_runtime_dependency(%q, [">= 0"])
41 | else
42 | s.add_dependency(%q, [">= 0"])
43 | end
44 | else
45 | s.add_dependency(%q, [">= 0"])
46 | end
47 | end
48 |
49 |
--------------------------------------------------------------------------------
/client/lib/juggernaut.rb:
--------------------------------------------------------------------------------
1 | require "redis"
2 | require "json"
3 |
4 | # Attempt to provide Engine to Rails
5 | require "juggernaut/rails/engine"
6 |
7 | module Juggernaut
8 | EVENTS = [
9 | "juggernaut:subscribe",
10 | "juggernaut:unsubscribe",
11 | "juggernaut:custom"
12 | ]
13 |
14 | def options
15 | @options ||= {}
16 | end
17 |
18 | def options=(val)
19 | @options = val
20 | end
21 |
22 | def url=(url)
23 | options[:url] = url
24 | end
25 |
26 | def publish(channels, data, options = {})
27 | message = ({:channels => Array(channels).uniq, :data => data}).merge(options)
28 | redis.publish(key, message.to_json)
29 | end
30 |
31 | def subscribe
32 | Redis.connect(options).subscribe(*EVENTS) do |on|
33 | on.message do |type, msg|
34 | yield(type.gsub(/^juggernaut:/, "").to_sym, JSON.parse(msg))
35 | end
36 | end
37 | end
38 |
39 | protected
40 | def redis
41 | @redis ||= Redis.connect(options)
42 | end
43 |
44 | def key(*args)
45 | args.unshift(:juggernaut).join(":")
46 | end
47 |
48 | extend self
49 | end
--------------------------------------------------------------------------------
/client/lib/juggernaut/rails/engine.rb:
--------------------------------------------------------------------------------
1 | module Juggernaut
2 | begin
3 | class Engine < ::Rails::Engine
4 | end
5 | rescue NameError
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/client/vendor/assets/javascripts/json.js:
--------------------------------------------------------------------------------
1 | /*
2 | http://www.JSON.org/json2.js
3 | 2011-02-23
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, strict: false, regexp: false */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | var JSON;
163 | if (!JSON) {
164 | JSON = {};
165 | }
166 |
167 | (function () {
168 | "use strict";
169 |
170 | function f(n) {
171 | // Format integers to have at least two digits.
172 | return n < 10 ? '0' + n : n;
173 | }
174 |
175 | if (typeof Date.prototype.toJSON !== 'function') {
176 |
177 | Date.prototype.toJSON = function (key) {
178 |
179 | return isFinite(this.valueOf()) ?
180 | this.getUTCFullYear() + '-' +
181 | f(this.getUTCMonth() + 1) + '-' +
182 | f(this.getUTCDate()) + 'T' +
183 | f(this.getUTCHours()) + ':' +
184 | f(this.getUTCMinutes()) + ':' +
185 | f(this.getUTCSeconds()) + 'Z' : null;
186 | };
187 |
188 | String.prototype.toJSON =
189 | Number.prototype.toJSON =
190 | Boolean.prototype.toJSON = function (key) {
191 | return this.valueOf();
192 | };
193 | }
194 |
195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197 | gap,
198 | indent,
199 | meta = { // table of character substitutions
200 | '\b': '\\b',
201 | '\t': '\\t',
202 | '\n': '\\n',
203 | '\f': '\\f',
204 | '\r': '\\r',
205 | '"' : '\\"',
206 | '\\': '\\\\'
207 | },
208 | rep;
209 |
210 |
211 | function quote(string) {
212 |
213 | // If the string contains no control characters, no quote characters, and no
214 | // backslash characters, then we can safely slap some quotes around it.
215 | // Otherwise we must also replace the offending characters with safe escape
216 | // sequences.
217 |
218 | escapable.lastIndex = 0;
219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
220 | var c = meta[a];
221 | return typeof c === 'string' ? c :
222 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
223 | }) + '"' : '"' + string + '"';
224 | }
225 |
226 |
227 | function str(key, holder) {
228 |
229 | // Produce a string from holder[key].
230 |
231 | var i, // The loop counter.
232 | k, // The member key.
233 | v, // The member value.
234 | length,
235 | mind = gap,
236 | partial,
237 | value = holder[key];
238 |
239 | // If the value has a toJSON method, call it to obtain a replacement value.
240 |
241 | if (value && typeof value === 'object' &&
242 | typeof value.toJSON === 'function') {
243 | value = value.toJSON(key);
244 | }
245 |
246 | // If we were called with a replacer function, then call the replacer to
247 | // obtain a replacement value.
248 |
249 | if (typeof rep === 'function') {
250 | value = rep.call(holder, key, value);
251 | }
252 |
253 | // What happens next depends on the value's type.
254 |
255 | switch (typeof value) {
256 | case 'string':
257 | return quote(value);
258 |
259 | case 'number':
260 |
261 | // JSON numbers must be finite. Encode non-finite numbers as null.
262 |
263 | return isFinite(value) ? String(value) : 'null';
264 |
265 | case 'boolean':
266 | case 'null':
267 |
268 | // If the value is a boolean or null, convert it to a string. Note:
269 | // typeof null does not produce 'null'. The case is included here in
270 | // the remote chance that this gets fixed someday.
271 |
272 | return String(value);
273 |
274 | // If the type is 'object', we might be dealing with an object or an array or
275 | // null.
276 |
277 | case 'object':
278 |
279 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
280 | // so watch out for that case.
281 |
282 | if (!value) {
283 | return 'null';
284 | }
285 |
286 | // Make an array to hold the partial results of stringifying this object value.
287 |
288 | gap += indent;
289 | partial = [];
290 |
291 | // Is the value an array?
292 |
293 | if (Object.prototype.toString.apply(value) === '[object Array]') {
294 |
295 | // The value is an array. Stringify every element. Use null as a placeholder
296 | // for non-JSON values.
297 |
298 | length = value.length;
299 | for (i = 0; i < length; i += 1) {
300 | partial[i] = str(i, value) || 'null';
301 | }
302 |
303 | // Join all of the elements together, separated with commas, and wrap them in
304 | // brackets.
305 |
306 | v = partial.length === 0 ? '[]' : gap ?
307 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
308 | '[' + partial.join(',') + ']';
309 | gap = mind;
310 | return v;
311 | }
312 |
313 | // If the replacer is an array, use it to select the members to be stringified.
314 |
315 | if (rep && typeof rep === 'object') {
316 | length = rep.length;
317 | for (i = 0; i < length; i += 1) {
318 | if (typeof rep[i] === 'string') {
319 | k = rep[i];
320 | v = str(k, value);
321 | if (v) {
322 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
323 | }
324 | }
325 | }
326 | } else {
327 |
328 | // Otherwise, iterate through all of the keys in the object.
329 |
330 | for (k in value) {
331 | if (Object.prototype.hasOwnProperty.call(value, k)) {
332 | v = str(k, value);
333 | if (v) {
334 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
335 | }
336 | }
337 | }
338 | }
339 |
340 | // Join all of the member texts together, separated with commas,
341 | // and wrap them in braces.
342 |
343 | v = partial.length === 0 ? '{}' : gap ?
344 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
345 | '{' + partial.join(',') + '}';
346 | gap = mind;
347 | return v;
348 | }
349 | }
350 |
351 | // If the JSON object does not yet have a stringify method, give it one.
352 |
353 | if (typeof JSON.stringify !== 'function') {
354 | JSON.stringify = function (value, replacer, space) {
355 |
356 | // The stringify method takes a value and an optional replacer, and an optional
357 | // space parameter, and returns a JSON text. The replacer can be a function
358 | // that can replace values, or an array of strings that will select the keys.
359 | // A default replacer method can be provided. Use of the space parameter can
360 | // produce text that is more easily readable.
361 |
362 | var i;
363 | gap = '';
364 | indent = '';
365 |
366 | // If the space parameter is a number, make an indent string containing that
367 | // many spaces.
368 |
369 | if (typeof space === 'number') {
370 | for (i = 0; i < space; i += 1) {
371 | indent += ' ';
372 | }
373 |
374 | // If the space parameter is a string, it will be used as the indent string.
375 |
376 | } else if (typeof space === 'string') {
377 | indent = space;
378 | }
379 |
380 | // If there is a replacer, it must be a function or an array.
381 | // Otherwise, throw an error.
382 |
383 | rep = replacer;
384 | if (replacer && typeof replacer !== 'function' &&
385 | (typeof replacer !== 'object' ||
386 | typeof replacer.length !== 'number')) {
387 | throw new Error('JSON.stringify');
388 | }
389 |
390 | // Make a fake root object containing our value under the key of ''.
391 | // Return the result of stringifying the value.
392 |
393 | return str('', {'': value});
394 | };
395 | }
396 |
397 |
398 | // If the JSON object does not yet have a parse method, give it one.
399 |
400 | if (typeof JSON.parse !== 'function') {
401 | JSON.parse = function (text, reviver) {
402 |
403 | // The parse method takes a text and an optional reviver function, and returns
404 | // a JavaScript value if the text is a valid JSON text.
405 |
406 | var j;
407 |
408 | function walk(holder, key) {
409 |
410 | // The walk method is used to recursively walk the resulting structure so
411 | // that modifications can be made.
412 |
413 | var k, v, value = holder[key];
414 | if (value && typeof value === 'object') {
415 | for (k in value) {
416 | if (Object.prototype.hasOwnProperty.call(value, k)) {
417 | v = walk(value, k);
418 | if (v !== undefined) {
419 | value[k] = v;
420 | } else {
421 | delete value[k];
422 | }
423 | }
424 | }
425 | }
426 | return reviver.call(holder, key, value);
427 | }
428 |
429 |
430 | // Parsing happens in four stages. In the first stage, we replace certain
431 | // Unicode characters with escape sequences. JavaScript handles many characters
432 | // incorrectly, either silently deleting them, or treating them as line endings.
433 |
434 | text = String(text);
435 | cx.lastIndex = 0;
436 | if (cx.test(text)) {
437 | text = text.replace(cx, function (a) {
438 | return '\\u' +
439 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
440 | });
441 | }
442 |
443 | // In the second stage, we run the text against regular expressions that look
444 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
445 | // because they can cause invocation, and '=' because it can cause mutation.
446 | // But just to be safe, we want to reject all unexpected forms.
447 |
448 | // We split the second stage into 4 regexp operations in order to work around
449 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
450 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
451 | // replace all simple value tokens with ']' characters. Third, we delete all
452 | // open brackets that follow a colon or comma or that begin the text. Finally,
453 | // we look to see that the remaining characters are only whitespace or ']' or
454 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
455 |
456 | if (/^[\],:{}\s]*$/
457 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
458 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
459 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
460 |
461 | // In the third stage we use the eval function to compile the text into a
462 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
463 | // in JavaScript: it can begin a block or an object literal. We wrap the text
464 | // in parens to eliminate the ambiguity.
465 |
466 | j = eval('(' + text + ')');
467 |
468 | // In the optional fourth stage, we recursively walk the new structure, passing
469 | // each name/value pair to a reviver function for possible transformation.
470 |
471 | return typeof reviver === 'function' ?
472 | walk({'': j}, '') : j;
473 | }
474 |
475 | // If the text is not JSON parseable, then a SyntaxError is thrown.
476 |
477 | throw new SyntaxError('JSON.parse');
478 | };
479 | }
480 | }());
--------------------------------------------------------------------------------
/client/vendor/assets/javascripts/juggernaut.js:
--------------------------------------------------------------------------------
1 | //= require socket_io
2 |
3 | var Juggernaut = function(options){
4 | this.options = options || {};
5 |
6 | this.options.host = this.options.host || window.location.hostname;
7 | this.options.port = this.options.port || 8080;
8 |
9 | this.handlers = {};
10 | this.meta = this.options.meta;
11 |
12 | this.io = io.connect(this.options.host, this.options);
13 |
14 | this.io.on("connect", this.proxy(this.onconnect));
15 | this.io.on("message", this.proxy(this.onmessage));
16 | this.io.on("disconnect", this.proxy(this.ondisconnect));
17 |
18 | this.on("connect", this.proxy(this.writeMeta));
19 | };
20 |
21 | // Helper methods
22 |
23 | Juggernaut.fn = Juggernaut.prototype;
24 | Juggernaut.fn.proxy = function(func){
25 | var thisObject = this;
26 | return(function(){ return func.apply(thisObject, arguments); });
27 | };
28 |
29 | // Public methods
30 |
31 | Juggernaut.fn.on = function(name, callback){
32 | if ( !name || !callback ) return;
33 | if ( !this.handlers[name] ) this.handlers[name] = [];
34 | this.handlers[name].push(callback);
35 | };
36 | Juggernaut.fn.bind = Juggernaut.fn.on;
37 |
38 | Juggernaut.fn.unbind = function(name){
39 | if (!this.handlers) return;
40 | delete this.handlers[name];
41 | };
42 |
43 | Juggernaut.fn.write = function(message){
44 | if (typeof message.toJSON == "function")
45 | message = message.toJSON();
46 |
47 | this.io.send(message);
48 | };
49 |
50 | Juggernaut.fn.subscribe = function(channel, callback){
51 | if ( !channel ) throw "Must provide a channel";
52 |
53 | this.on(channel + ":data", callback);
54 |
55 | var connectCallback = this.proxy(function(){
56 | var message = new Juggernaut.Message;
57 | message.type = "subscribe";
58 | message.channel = channel;
59 |
60 | this.write(message);
61 | });
62 |
63 | if (this.io.socket.connected)
64 | connectCallback();
65 | else {
66 | this.on("connect", connectCallback);
67 | }
68 | };
69 |
70 | Juggernaut.fn.unsubscribe = function(channel) {
71 | if ( !channel ) throw "Must provide a channel";
72 |
73 | this.unbind(channel + ":data");
74 |
75 | var message = new Juggernaut.Message;
76 | message.type = "unsubscribe";
77 | message.channel = channel;
78 |
79 | this.write(message);
80 | };
81 |
82 | // Private
83 |
84 | Juggernaut.fn.trigger = function(){
85 | var args = [];
86 | for (var f=0; f < arguments.length; f++) args.push(arguments[f]);
87 |
88 | var name = args.shift();
89 |
90 | var callbacks = this.handlers[name];
91 | if ( !callbacks ) return;
92 |
93 | for(var i=0, len = callbacks.length; i < len; i++)
94 | callbacks[i].apply(this, args);
95 | };
96 |
97 | Juggernaut.fn.writeMeta = function(){
98 | if ( !this.meta ) return;
99 | var message = new Juggernaut.Message;
100 | message.type = "meta";
101 | message.data = this.meta;
102 | this.write(message);
103 | };
104 |
105 | Juggernaut.fn.onconnect = function(){
106 | this.sessionID = this.io.socket.sessionid;
107 | this.trigger("connect");
108 | };
109 |
110 | Juggernaut.fn.ondisconnect = function(){
111 | this.trigger("disconnect");
112 | };
113 |
114 | Juggernaut.fn.onmessage = function(data){
115 | var message = Juggernaut.Message.fromJSON(data);
116 | this.trigger("message", message);
117 | this.trigger("data", message.channel, message.data);
118 | this.trigger(message.channel + ":data", message.data);
119 | };
120 |
121 | Juggernaut.Message = function(hash){
122 | for (var key in hash) this[key] = hash[key];
123 | };
124 |
125 | Juggernaut.Message.fromJSON = function(json){
126 | return(new this(JSON.parse(json)))
127 | };
128 |
129 | Juggernaut.Message.prototype.toJSON = function(){
130 | var object = {};
131 | for (var key in this) {
132 | if (typeof this[key] != "function")
133 | object[key] = this[key];
134 | }
135 | return(JSON.stringify(object));
136 | };
137 |
138 | if (typeof module != "undefined") {
139 | module.exports = Juggernaut;
140 | } else {
141 | window.Juggernaut = Juggernaut;
142 | }
--------------------------------------------------------------------------------
/client/vendor/assets/javascripts/socket_io.js:
--------------------------------------------------------------------------------
1 | /*! Socket.IO.js build:0.8.7, development. Copyright(c) 2011 LearnBoost MIT Licensed */
2 |
3 | /**
4 | * socket.io
5 | * Copyright(c) 2011 LearnBoost
6 | * MIT Licensed
7 | */
8 |
9 | (function (exports, global) {
10 |
11 | /**
12 | * IO namespace.
13 | *
14 | * @namespace
15 | */
16 |
17 | var io = exports;
18 |
19 | /**
20 | * Socket.IO version
21 | *
22 | * @api public
23 | */
24 |
25 | io.version = '0.8.7';
26 |
27 | /**
28 | * Protocol implemented.
29 | *
30 | * @api public
31 | */
32 |
33 | io.protocol = 1;
34 |
35 | /**
36 | * Available transports, these will be populated with the available transports
37 | *
38 | * @api public
39 | */
40 |
41 | io.transports = [];
42 |
43 | /**
44 | * Keep track of jsonp callbacks.
45 | *
46 | * @api private
47 | */
48 |
49 | io.j = [];
50 |
51 | /**
52 | * Keep track of our io.Sockets
53 | *
54 | * @api private
55 | */
56 | io.sockets = {};
57 |
58 |
59 | /**
60 | * Manages connections to hosts.
61 | *
62 | * @param {String} uri
63 | * @Param {Boolean} force creation of new socket (defaults to false)
64 | * @api public
65 | */
66 |
67 | io.connect = function (host, details) {
68 | var uri = io.util.parseUri(host)
69 | , uuri
70 | , socket;
71 |
72 | if (global && global.location) {
73 | uri.protocol = uri.protocol || global.location.protocol.slice(0, -1);
74 | uri.host = uri.host || (global.document
75 | ? global.document.domain : global.location.hostname);
76 | uri.port = uri.port || global.location.port;
77 | }
78 |
79 | uuri = io.util.uniqueUri(uri);
80 |
81 | var options = {
82 | host: uri.host
83 | , secure: 'https' == uri.protocol
84 | , port: uri.port || ('https' == uri.protocol ? 443 : 80)
85 | , query: uri.query || ''
86 | };
87 |
88 | io.util.merge(options, details);
89 |
90 | if (options['force new connection'] || !io.sockets[uuri]) {
91 | socket = new io.Socket(options);
92 | }
93 |
94 | if (!options['force new connection'] && socket) {
95 | io.sockets[uuri] = socket;
96 | }
97 |
98 | socket = socket || io.sockets[uuri];
99 |
100 | // if path is different from '' or /
101 | return socket.of(uri.path.length > 1 ? uri.path : '');
102 | };
103 |
104 | })('object' === typeof module ? module.exports : (this.io = {}), this);
105 |
106 | /**
107 | * socket.io
108 | * Copyright(c) 2011 LearnBoost
109 | * MIT Licensed
110 | */
111 |
112 | (function (exports, global) {
113 |
114 | /**
115 | * Utilities namespace.
116 | *
117 | * @namespace
118 | */
119 |
120 | var util = exports.util = {};
121 |
122 | /**
123 | * Parses an URI
124 | *
125 | * @author Steven Levithan (MIT license)
126 | * @api public
127 | */
128 |
129 | var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
130 |
131 | var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password',
132 | 'host', 'port', 'relative', 'path', 'directory', 'file', 'query',
133 | 'anchor'];
134 |
135 | util.parseUri = function (str) {
136 | var m = re.exec(str || '')
137 | , uri = {}
138 | , i = 14;
139 |
140 | while (i--) {
141 | uri[parts[i]] = m[i] || '';
142 | }
143 |
144 | return uri;
145 | };
146 |
147 | /**
148 | * Produces a unique url that identifies a Socket.IO connection.
149 | *
150 | * @param {Object} uri
151 | * @api public
152 | */
153 |
154 | util.uniqueUri = function (uri) {
155 | var protocol = uri.protocol
156 | , host = uri.host
157 | , port = uri.port;
158 |
159 | if ('document' in global) {
160 | host = host || document.domain;
161 | port = port || (protocol == 'https'
162 | && document.location.protocol !== 'https:' ? 443 : document.location.port);
163 | } else {
164 | host = host || 'localhost';
165 |
166 | if (!port && protocol == 'https') {
167 | port = 443;
168 | }
169 | }
170 |
171 | return (protocol || 'http') + '://' + host + ':' + (port || 80);
172 | };
173 |
174 | /**
175 | * Mergest 2 query strings in to once unique query string
176 | *
177 | * @param {String} base
178 | * @param {String} addition
179 | * @api public
180 | */
181 |
182 | util.query = function (base, addition) {
183 | var query = util.chunkQuery(base || '')
184 | , components = [];
185 |
186 | util.merge(query, util.chunkQuery(addition || ''));
187 | for (var part in query) {
188 | if (query.hasOwnProperty(part)) {
189 | components.push(part + '=' + query[part]);
190 | }
191 | }
192 |
193 | return components.length ? '?' + components.join('&') : '';
194 | };
195 |
196 | /**
197 | * Transforms a querystring in to an object
198 | *
199 | * @param {String} qs
200 | * @api public
201 | */
202 |
203 | util.chunkQuery = function (qs) {
204 | var query = {}
205 | , params = qs.split('&')
206 | , i = 0
207 | , l = params.length
208 | , kv;
209 |
210 | for (; i < l; ++i) {
211 | kv = params[i].split('=');
212 | if (kv[0]) {
213 | query[kv[0]] = decodeURIComponent(kv[1]);
214 | }
215 | }
216 |
217 | return query;
218 | };
219 |
220 | /**
221 | * Executes the given function when the page is loaded.
222 | *
223 | * io.util.load(function () { console.log('page loaded'); });
224 | *
225 | * @param {Function} fn
226 | * @api public
227 | */
228 |
229 | var pageLoaded = false;
230 |
231 | util.load = function (fn) {
232 | if ('document' in global && document.readyState === 'complete' || pageLoaded) {
233 | return fn();
234 | }
235 |
236 | util.on(global, 'load', fn, false);
237 | };
238 |
239 | /**
240 | * Adds an event.
241 | *
242 | * @api private
243 | */
244 |
245 | util.on = function (element, event, fn, capture) {
246 | if (element.attachEvent) {
247 | element.attachEvent('on' + event, fn);
248 | } else if (element.addEventListener) {
249 | element.addEventListener(event, fn, capture);
250 | }
251 | };
252 |
253 | /**
254 | * Generates the correct `XMLHttpRequest` for regular and cross domain requests.
255 | *
256 | * @param {Boolean} [xdomain] Create a request that can be used cross domain.
257 | * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest.
258 | * @api private
259 | */
260 |
261 | util.request = function (xdomain) {
262 |
263 | if (xdomain && 'undefined' != typeof XDomainRequest) {
264 | return new XDomainRequest();
265 | }
266 |
267 | if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) {
268 | return new XMLHttpRequest();
269 | }
270 |
271 | if (!xdomain) {
272 | try {
273 | return new ActiveXObject('Microsoft.XMLHTTP');
274 | } catch(e) { }
275 | }
276 |
277 | return null;
278 | };
279 |
280 | /**
281 | * XHR based transport constructor.
282 | *
283 | * @constructor
284 | * @api public
285 | */
286 |
287 | /**
288 | * Change the internal pageLoaded value.
289 | */
290 |
291 | if ('undefined' != typeof window) {
292 | util.load(function () {
293 | pageLoaded = true;
294 | });
295 | }
296 |
297 | /**
298 | * Defers a function to ensure a spinner is not displayed by the browser
299 | *
300 | * @param {Function} fn
301 | * @api public
302 | */
303 |
304 | util.defer = function (fn) {
305 | if (!util.ua.webkit || 'undefined' != typeof importScripts) {
306 | return fn();
307 | }
308 |
309 | util.load(function () {
310 | setTimeout(fn, 100);
311 | });
312 | };
313 |
314 | /**
315 | * Merges two objects.
316 | *
317 | * @api public
318 | */
319 |
320 | util.merge = function merge (target, additional, deep, lastseen) {
321 | var seen = lastseen || []
322 | , depth = typeof deep == 'undefined' ? 2 : deep
323 | , prop;
324 |
325 | for (prop in additional) {
326 | if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) {
327 | if (typeof target[prop] !== 'object' || !depth) {
328 | target[prop] = additional[prop];
329 | seen.push(additional[prop]);
330 | } else {
331 | util.merge(target[prop], additional[prop], depth - 1, seen);
332 | }
333 | }
334 | }
335 |
336 | return target;
337 | };
338 |
339 | /**
340 | * Merges prototypes from objects
341 | *
342 | * @api public
343 | */
344 |
345 | util.mixin = function (ctor, ctor2) {
346 | util.merge(ctor.prototype, ctor2.prototype);
347 | };
348 |
349 | /**
350 | * Shortcut for prototypical and static inheritance.
351 | *
352 | * @api private
353 | */
354 |
355 | util.inherit = function (ctor, ctor2) {
356 | function f() {};
357 | f.prototype = ctor2.prototype;
358 | ctor.prototype = new f;
359 | };
360 |
361 | /**
362 | * Checks if the given object is an Array.
363 | *
364 | * io.util.isArray([]); // true
365 | * io.util.isArray({}); // false
366 | *
367 | * @param Object obj
368 | * @api public
369 | */
370 |
371 | util.isArray = Array.isArray || function (obj) {
372 | return Object.prototype.toString.call(obj) === '[object Array]';
373 | };
374 |
375 | /**
376 | * Intersects values of two arrays into a third
377 | *
378 | * @api public
379 | */
380 |
381 | util.intersect = function (arr, arr2) {
382 | var ret = []
383 | , longest = arr.length > arr2.length ? arr : arr2
384 | , shortest = arr.length > arr2.length ? arr2 : arr;
385 |
386 | for (var i = 0, l = shortest.length; i < l; i++) {
387 | if (~util.indexOf(longest, shortest[i]))
388 | ret.push(shortest[i]);
389 | }
390 |
391 | return ret;
392 | }
393 |
394 | /**
395 | * Array indexOf compatibility.
396 | *
397 | * @see bit.ly/a5Dxa2
398 | * @api public
399 | */
400 |
401 | util.indexOf = function (arr, o, i) {
402 | if (Array.prototype.indexOf) {
403 | return Array.prototype.indexOf.call(arr, o, i);
404 | }
405 |
406 | for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0;
407 | i < j && arr[i] !== o; i++) {}
408 |
409 | return j <= i ? -1 : i;
410 | };
411 |
412 | /**
413 | * Converts enumerables to array.
414 | *
415 | * @api public
416 | */
417 |
418 | util.toArray = function (enu) {
419 | var arr = [];
420 |
421 | for (var i = 0, l = enu.length; i < l; i++)
422 | arr.push(enu[i]);
423 |
424 | return arr;
425 | };
426 |
427 | /**
428 | * UA / engines detection namespace.
429 | *
430 | * @namespace
431 | */
432 |
433 | util.ua = {};
434 |
435 | /**
436 | * Whether the UA supports CORS for XHR.
437 | *
438 | * @api public
439 | */
440 |
441 | util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () {
442 | try {
443 | var a = new XMLHttpRequest();
444 | } catch (e) {
445 | return false;
446 | }
447 |
448 | return a.withCredentials != undefined;
449 | })();
450 |
451 | /**
452 | * Detect webkit.
453 | *
454 | * @api public
455 | */
456 |
457 | util.ua.webkit = 'undefined' != typeof navigator
458 | && /webkit/i.test(navigator.userAgent);
459 |
460 | })('undefined' != typeof io ? io : module.exports, this);
461 |
462 | /**
463 | * socket.io
464 | * Copyright(c) 2011 LearnBoost
465 | * MIT Licensed
466 | */
467 |
468 | (function (exports, io) {
469 |
470 | /**
471 | * Expose constructor.
472 | */
473 |
474 | exports.EventEmitter = EventEmitter;
475 |
476 | /**
477 | * Event emitter constructor.
478 | *
479 | * @api public.
480 | */
481 |
482 | function EventEmitter () {};
483 |
484 | /**
485 | * Adds a listener
486 | *
487 | * @api public
488 | */
489 |
490 | EventEmitter.prototype.on = function (name, fn) {
491 | if (!this.$events) {
492 | this.$events = {};
493 | }
494 |
495 | if (!this.$events[name]) {
496 | this.$events[name] = fn;
497 | } else if (io.util.isArray(this.$events[name])) {
498 | this.$events[name].push(fn);
499 | } else {
500 | this.$events[name] = [this.$events[name], fn];
501 | }
502 |
503 | return this;
504 | };
505 |
506 | EventEmitter.prototype.addListener = EventEmitter.prototype.on;
507 |
508 | /**
509 | * Adds a volatile listener.
510 | *
511 | * @api public
512 | */
513 |
514 | EventEmitter.prototype.once = function (name, fn) {
515 | var self = this;
516 |
517 | function on () {
518 | self.removeListener(name, on);
519 | fn.apply(this, arguments);
520 | };
521 |
522 | on.listener = fn;
523 | this.on(name, on);
524 |
525 | return this;
526 | };
527 |
528 | /**
529 | * Removes a listener.
530 | *
531 | * @api public
532 | */
533 |
534 | EventEmitter.prototype.removeListener = function (name, fn) {
535 | if (this.$events && this.$events[name]) {
536 | var list = this.$events[name];
537 |
538 | if (io.util.isArray(list)) {
539 | var pos = -1;
540 |
541 | for (var i = 0, l = list.length; i < l; i++) {
542 | if (list[i] === fn || (list[i].listener && list[i].listener === fn)) {
543 | pos = i;
544 | break;
545 | }
546 | }
547 |
548 | if (pos < 0) {
549 | return this;
550 | }
551 |
552 | list.splice(pos, 1);
553 |
554 | if (!list.length) {
555 | delete this.$events[name];
556 | }
557 | } else if (list === fn || (list.listener && list.listener === fn)) {
558 | delete this.$events[name];
559 | }
560 | }
561 |
562 | return this;
563 | };
564 |
565 | /**
566 | * Removes all listeners for an event.
567 | *
568 | * @api public
569 | */
570 |
571 | EventEmitter.prototype.removeAllListeners = function (name) {
572 | // TODO: enable this when node 0.5 is stable
573 | //if (name === undefined) {
574 | //this.$events = {};
575 | //return this;
576 | //}
577 |
578 | if (this.$events && this.$events[name]) {
579 | this.$events[name] = null;
580 | }
581 |
582 | return this;
583 | };
584 |
585 | /**
586 | * Gets all listeners for a certain event.
587 | *
588 | * @api publci
589 | */
590 |
591 | EventEmitter.prototype.listeners = function (name) {
592 | if (!this.$events) {
593 | this.$events = {};
594 | }
595 |
596 | if (!this.$events[name]) {
597 | this.$events[name] = [];
598 | }
599 |
600 | if (!io.util.isArray(this.$events[name])) {
601 | this.$events[name] = [this.$events[name]];
602 | }
603 |
604 | return this.$events[name];
605 | };
606 |
607 | /**
608 | * Emits an event.
609 | *
610 | * @api public
611 | */
612 |
613 | EventEmitter.prototype.emit = function (name) {
614 | if (!this.$events) {
615 | return false;
616 | }
617 |
618 | var handler = this.$events[name];
619 |
620 | if (!handler) {
621 | return false;
622 | }
623 |
624 | var args = Array.prototype.slice.call(arguments, 1);
625 |
626 | if ('function' == typeof handler) {
627 | handler.apply(this, args);
628 | } else if (io.util.isArray(handler)) {
629 | var listeners = handler.slice();
630 |
631 | for (var i = 0, l = listeners.length; i < l; i++) {
632 | listeners[i].apply(this, args);
633 | }
634 | } else {
635 | return false;
636 | }
637 |
638 | return true;
639 | };
640 |
641 | })(
642 | 'undefined' != typeof io ? io : module.exports
643 | , 'undefined' != typeof io ? io : module.parent.exports
644 | );
645 |
646 | /**
647 | * socket.io
648 | * Copyright(c) 2011 LearnBoost
649 | * MIT Licensed
650 | */
651 |
652 | /**
653 | * Based on JSON2 (http://www.JSON.org/js.html).
654 | */
655 |
656 | (function (exports, nativeJSON) {
657 | "use strict";
658 |
659 | // use native JSON if it's available
660 | if (nativeJSON && nativeJSON.parse){
661 | return exports.JSON = {
662 | parse: nativeJSON.parse
663 | , stringify: nativeJSON.stringify
664 | }
665 | }
666 |
667 | var JSON = exports.JSON = {};
668 |
669 | function f(n) {
670 | // Format integers to have at least two digits.
671 | return n < 10 ? '0' + n : n;
672 | }
673 |
674 | function date(d, key) {
675 | return isFinite(d.valueOf()) ?
676 | d.getUTCFullYear() + '-' +
677 | f(d.getUTCMonth() + 1) + '-' +
678 | f(d.getUTCDate()) + 'T' +
679 | f(d.getUTCHours()) + ':' +
680 | f(d.getUTCMinutes()) + ':' +
681 | f(d.getUTCSeconds()) + 'Z' : null;
682 | };
683 |
684 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
685 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
686 | gap,
687 | indent,
688 | meta = { // table of character substitutions
689 | '\b': '\\b',
690 | '\t': '\\t',
691 | '\n': '\\n',
692 | '\f': '\\f',
693 | '\r': '\\r',
694 | '"' : '\\"',
695 | '\\': '\\\\'
696 | },
697 | rep;
698 |
699 |
700 | function quote(string) {
701 |
702 | // If the string contains no control characters, no quote characters, and no
703 | // backslash characters, then we can safely slap some quotes around it.
704 | // Otherwise we must also replace the offending characters with safe escape
705 | // sequences.
706 |
707 | escapable.lastIndex = 0;
708 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
709 | var c = meta[a];
710 | return typeof c === 'string' ? c :
711 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
712 | }) + '"' : '"' + string + '"';
713 | }
714 |
715 |
716 | function str(key, holder) {
717 |
718 | // Produce a string from holder[key].
719 |
720 | var i, // The loop counter.
721 | k, // The member key.
722 | v, // The member value.
723 | length,
724 | mind = gap,
725 | partial,
726 | value = holder[key];
727 |
728 | // If the value has a toJSON method, call it to obtain a replacement value.
729 |
730 | if (value instanceof Date) {
731 | value = date(key);
732 | }
733 |
734 | // If we were called with a replacer function, then call the replacer to
735 | // obtain a replacement value.
736 |
737 | if (typeof rep === 'function') {
738 | value = rep.call(holder, key, value);
739 | }
740 |
741 | // What happens next depends on the value's type.
742 |
743 | switch (typeof value) {
744 | case 'string':
745 | return quote(value);
746 |
747 | case 'number':
748 |
749 | // JSON numbers must be finite. Encode non-finite numbers as null.
750 |
751 | return isFinite(value) ? String(value) : 'null';
752 |
753 | case 'boolean':
754 | case 'null':
755 |
756 | // If the value is a boolean or null, convert it to a string. Note:
757 | // typeof null does not produce 'null'. The case is included here in
758 | // the remote chance that this gets fixed someday.
759 |
760 | return String(value);
761 |
762 | // If the type is 'object', we might be dealing with an object or an array or
763 | // null.
764 |
765 | case 'object':
766 |
767 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
768 | // so watch out for that case.
769 |
770 | if (!value) {
771 | return 'null';
772 | }
773 |
774 | // Make an array to hold the partial results of stringifying this object value.
775 |
776 | gap += indent;
777 | partial = [];
778 |
779 | // Is the value an array?
780 |
781 | if (Object.prototype.toString.apply(value) === '[object Array]') {
782 |
783 | // The value is an array. Stringify every element. Use null as a placeholder
784 | // for non-JSON values.
785 |
786 | length = value.length;
787 | for (i = 0; i < length; i += 1) {
788 | partial[i] = str(i, value) || 'null';
789 | }
790 |
791 | // Join all of the elements together, separated with commas, and wrap them in
792 | // brackets.
793 |
794 | v = partial.length === 0 ? '[]' : gap ?
795 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
796 | '[' + partial.join(',') + ']';
797 | gap = mind;
798 | return v;
799 | }
800 |
801 | // If the replacer is an array, use it to select the members to be stringified.
802 |
803 | if (rep && typeof rep === 'object') {
804 | length = rep.length;
805 | for (i = 0; i < length; i += 1) {
806 | if (typeof rep[i] === 'string') {
807 | k = rep[i];
808 | v = str(k, value);
809 | if (v) {
810 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
811 | }
812 | }
813 | }
814 | } else {
815 |
816 | // Otherwise, iterate through all of the keys in the object.
817 |
818 | for (k in value) {
819 | if (Object.prototype.hasOwnProperty.call(value, k)) {
820 | v = str(k, value);
821 | if (v) {
822 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
823 | }
824 | }
825 | }
826 | }
827 |
828 | // Join all of the member texts together, separated with commas,
829 | // and wrap them in braces.
830 |
831 | v = partial.length === 0 ? '{}' : gap ?
832 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
833 | '{' + partial.join(',') + '}';
834 | gap = mind;
835 | return v;
836 | }
837 | }
838 |
839 | // If the JSON object does not yet have a stringify method, give it one.
840 |
841 | JSON.stringify = function (value, replacer, space) {
842 |
843 | // The stringify method takes a value and an optional replacer, and an optional
844 | // space parameter, and returns a JSON text. The replacer can be a function
845 | // that can replace values, or an array of strings that will select the keys.
846 | // A default replacer method can be provided. Use of the space parameter can
847 | // produce text that is more easily readable.
848 |
849 | var i;
850 | gap = '';
851 | indent = '';
852 |
853 | // If the space parameter is a number, make an indent string containing that
854 | // many spaces.
855 |
856 | if (typeof space === 'number') {
857 | for (i = 0; i < space; i += 1) {
858 | indent += ' ';
859 | }
860 |
861 | // If the space parameter is a string, it will be used as the indent string.
862 |
863 | } else if (typeof space === 'string') {
864 | indent = space;
865 | }
866 |
867 | // If there is a replacer, it must be a function or an array.
868 | // Otherwise, throw an error.
869 |
870 | rep = replacer;
871 | if (replacer && typeof replacer !== 'function' &&
872 | (typeof replacer !== 'object' ||
873 | typeof replacer.length !== 'number')) {
874 | throw new Error('JSON.stringify');
875 | }
876 |
877 | // Make a fake root object containing our value under the key of ''.
878 | // Return the result of stringifying the value.
879 |
880 | return str('', {'': value});
881 | };
882 |
883 | // If the JSON object does not yet have a parse method, give it one.
884 |
885 | JSON.parse = function (text, reviver) {
886 | // The parse method takes a text and an optional reviver function, and returns
887 | // a JavaScript value if the text is a valid JSON text.
888 |
889 | var j;
890 |
891 | function walk(holder, key) {
892 |
893 | // The walk method is used to recursively walk the resulting structure so
894 | // that modifications can be made.
895 |
896 | var k, v, value = holder[key];
897 | if (value && typeof value === 'object') {
898 | for (k in value) {
899 | if (Object.prototype.hasOwnProperty.call(value, k)) {
900 | v = walk(value, k);
901 | if (v !== undefined) {
902 | value[k] = v;
903 | } else {
904 | delete value[k];
905 | }
906 | }
907 | }
908 | }
909 | return reviver.call(holder, key, value);
910 | }
911 |
912 |
913 | // Parsing happens in four stages. In the first stage, we replace certain
914 | // Unicode characters with escape sequences. JavaScript handles many characters
915 | // incorrectly, either silently deleting them, or treating them as line endings.
916 |
917 | text = String(text);
918 | cx.lastIndex = 0;
919 | if (cx.test(text)) {
920 | text = text.replace(cx, function (a) {
921 | return '\\u' +
922 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
923 | });
924 | }
925 |
926 | // In the second stage, we run the text against regular expressions that look
927 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
928 | // because they can cause invocation, and '=' because it can cause mutation.
929 | // But just to be safe, we want to reject all unexpected forms.
930 |
931 | // We split the second stage into 4 regexp operations in order to work around
932 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
933 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
934 | // replace all simple value tokens with ']' characters. Third, we delete all
935 | // open brackets that follow a colon or comma or that begin the text. Finally,
936 | // we look to see that the remaining characters are only whitespace or ']' or
937 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
938 |
939 | if (/^[\],:{}\s]*$/
940 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
941 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
942 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
943 |
944 | // In the third stage we use the eval function to compile the text into a
945 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
946 | // in JavaScript: it can begin a block or an object literal. We wrap the text
947 | // in parens to eliminate the ambiguity.
948 |
949 | j = eval('(' + text + ')');
950 |
951 | // In the optional fourth stage, we recursively walk the new structure, passing
952 | // each name/value pair to a reviver function for possible transformation.
953 |
954 | return typeof reviver === 'function' ?
955 | walk({'': j}, '') : j;
956 | }
957 |
958 | // If the text is not JSON parseable, then a SyntaxError is thrown.
959 |
960 | throw new SyntaxError('JSON.parse');
961 | };
962 |
963 | })(
964 | 'undefined' != typeof io ? io : module.exports
965 | , typeof JSON !== 'undefined' ? JSON : undefined
966 | );
967 |
968 | /**
969 | * socket.io
970 | * Copyright(c) 2011 LearnBoost
971 | * MIT Licensed
972 | */
973 |
974 | (function (exports, io) {
975 |
976 | /**
977 | * Parser namespace.
978 | *
979 | * @namespace
980 | */
981 |
982 | var parser = exports.parser = {};
983 |
984 | /**
985 | * Packet types.
986 | */
987 |
988 | var packets = parser.packets = [
989 | 'disconnect'
990 | , 'connect'
991 | , 'heartbeat'
992 | , 'message'
993 | , 'json'
994 | , 'event'
995 | , 'ack'
996 | , 'error'
997 | , 'noop'
998 | ];
999 |
1000 | /**
1001 | * Errors reasons.
1002 | */
1003 |
1004 | var reasons = parser.reasons = [
1005 | 'transport not supported'
1006 | , 'client not handshaken'
1007 | , 'unauthorized'
1008 | ];
1009 |
1010 | /**
1011 | * Errors advice.
1012 | */
1013 |
1014 | var advice = parser.advice = [
1015 | 'reconnect'
1016 | ];
1017 |
1018 | /**
1019 | * Shortcuts.
1020 | */
1021 |
1022 | var JSON = io.JSON
1023 | , indexOf = io.util.indexOf;
1024 |
1025 | /**
1026 | * Encodes a packet.
1027 | *
1028 | * @api private
1029 | */
1030 |
1031 | parser.encodePacket = function (packet) {
1032 | var type = indexOf(packets, packet.type)
1033 | , id = packet.id || ''
1034 | , endpoint = packet.endpoint || ''
1035 | , ack = packet.ack
1036 | , data = null;
1037 |
1038 | switch (packet.type) {
1039 | case 'error':
1040 | var reason = packet.reason ? indexOf(reasons, packet.reason) : ''
1041 | , adv = packet.advice ? indexOf(advice, packet.advice) : '';
1042 |
1043 | if (reason !== '' || adv !== '')
1044 | data = reason + (adv !== '' ? ('+' + adv) : '');
1045 |
1046 | break;
1047 |
1048 | case 'message':
1049 | if (packet.data !== '')
1050 | data = packet.data;
1051 | break;
1052 |
1053 | case 'event':
1054 | var ev = { name: packet.name };
1055 |
1056 | if (packet.args && packet.args.length) {
1057 | ev.args = packet.args;
1058 | }
1059 |
1060 | data = JSON.stringify(ev);
1061 | break;
1062 |
1063 | case 'json':
1064 | data = JSON.stringify(packet.data);
1065 | break;
1066 |
1067 | case 'connect':
1068 | if (packet.qs)
1069 | data = packet.qs;
1070 | break;
1071 |
1072 | case 'ack':
1073 | data = packet.ackId
1074 | + (packet.args && packet.args.length
1075 | ? '+' + JSON.stringify(packet.args) : '');
1076 | break;
1077 | }
1078 |
1079 | // construct packet with required fragments
1080 | var encoded = [
1081 | type
1082 | , id + (ack == 'data' ? '+' : '')
1083 | , endpoint
1084 | ];
1085 |
1086 | // data fragment is optional
1087 | if (data !== null && data !== undefined)
1088 | encoded.push(data);
1089 |
1090 | return encoded.join(':');
1091 | };
1092 |
1093 | /**
1094 | * Encodes multiple messages (payload).
1095 | *
1096 | * @param {Array} messages
1097 | * @api private
1098 | */
1099 |
1100 | parser.encodePayload = function (packets) {
1101 | var decoded = '';
1102 |
1103 | if (packets.length == 1)
1104 | return packets[0];
1105 |
1106 | for (var i = 0, l = packets.length; i < l; i++) {
1107 | var packet = packets[i];
1108 | decoded += '\ufffd' + packet.length + '\ufffd' + packets[i];
1109 | }
1110 |
1111 | return decoded;
1112 | };
1113 |
1114 | /**
1115 | * Decodes a packet
1116 | *
1117 | * @api private
1118 | */
1119 |
1120 | var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/;
1121 |
1122 | parser.decodePacket = function (data) {
1123 | var pieces = data.match(regexp);
1124 |
1125 | if (!pieces) return {};
1126 |
1127 | var id = pieces[2] || ''
1128 | , data = pieces[5] || ''
1129 | , packet = {
1130 | type: packets[pieces[1]]
1131 | , endpoint: pieces[4] || ''
1132 | };
1133 |
1134 | // whether we need to acknowledge the packet
1135 | if (id) {
1136 | packet.id = id;
1137 | if (pieces[3])
1138 | packet.ack = 'data';
1139 | else
1140 | packet.ack = true;
1141 | }
1142 |
1143 | // handle different packet types
1144 | switch (packet.type) {
1145 | case 'error':
1146 | var pieces = data.split('+');
1147 | packet.reason = reasons[pieces[0]] || '';
1148 | packet.advice = advice[pieces[1]] || '';
1149 | break;
1150 |
1151 | case 'message':
1152 | packet.data = data || '';
1153 | break;
1154 |
1155 | case 'event':
1156 | try {
1157 | var opts = JSON.parse(data);
1158 | packet.name = opts.name;
1159 | packet.args = opts.args;
1160 | } catch (e) { }
1161 |
1162 | packet.args = packet.args || [];
1163 | break;
1164 |
1165 | case 'json':
1166 | try {
1167 | packet.data = JSON.parse(data);
1168 | } catch (e) { }
1169 | break;
1170 |
1171 | case 'connect':
1172 | packet.qs = data || '';
1173 | break;
1174 |
1175 | case 'ack':
1176 | var pieces = data.match(/^([0-9]+)(\+)?(.*)/);
1177 | if (pieces) {
1178 | packet.ackId = pieces[1];
1179 | packet.args = [];
1180 |
1181 | if (pieces[3]) {
1182 | try {
1183 | packet.args = pieces[3] ? JSON.parse(pieces[3]) : [];
1184 | } catch (e) { }
1185 | }
1186 | }
1187 | break;
1188 |
1189 | case 'disconnect':
1190 | case 'heartbeat':
1191 | break;
1192 | };
1193 |
1194 | return packet;
1195 | };
1196 |
1197 | /**
1198 | * Decodes data payload. Detects multiple messages
1199 | *
1200 | * @return {Array} messages
1201 | * @api public
1202 | */
1203 |
1204 | parser.decodePayload = function (data) {
1205 | // IE doesn't like data[i] for unicode chars, charAt works fine
1206 | if (data.charAt(0) == '\ufffd') {
1207 | var ret = [];
1208 |
1209 | for (var i = 1, length = ''; i < data.length; i++) {
1210 | if (data.charAt(i) == '\ufffd') {
1211 | ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length)));
1212 | i += Number(length) + 1;
1213 | length = '';
1214 | } else {
1215 | length += data.charAt(i);
1216 | }
1217 | }
1218 |
1219 | return ret;
1220 | } else {
1221 | return [parser.decodePacket(data)];
1222 | }
1223 | };
1224 |
1225 | })(
1226 | 'undefined' != typeof io ? io : module.exports
1227 | , 'undefined' != typeof io ? io : module.parent.exports
1228 | );
1229 | /**
1230 | * socket.io
1231 | * Copyright(c) 2011 LearnBoost
1232 | * MIT Licensed
1233 | */
1234 |
1235 | (function (exports, io) {
1236 |
1237 | /**
1238 | * Expose constructor.
1239 | */
1240 |
1241 | exports.Transport = Transport;
1242 |
1243 | /**
1244 | * This is the transport template for all supported transport methods.
1245 | *
1246 | * @constructor
1247 | * @api public
1248 | */
1249 |
1250 | function Transport (socket, sessid) {
1251 | this.socket = socket;
1252 | this.sessid = sessid;
1253 | };
1254 |
1255 | /**
1256 | * Apply EventEmitter mixin.
1257 | */
1258 |
1259 | io.util.mixin(Transport, io.EventEmitter);
1260 |
1261 | /**
1262 | * Handles the response from the server. When a new response is received
1263 | * it will automatically update the timeout, decode the message and
1264 | * forwards the response to the onMessage function for further processing.
1265 | *
1266 | * @param {String} data Response from the server.
1267 | * @api private
1268 | */
1269 |
1270 | Transport.prototype.onData = function (data) {
1271 | this.clearCloseTimeout();
1272 |
1273 | // If the connection in currently open (or in a reopening state) reset the close
1274 | // timeout since we have just received data. This check is necessary so
1275 | // that we don't reset the timeout on an explicitly disconnected connection.
1276 | if (this.connected || this.connecting || this.reconnecting) {
1277 | this.setCloseTimeout();
1278 | }
1279 |
1280 | if (data !== '') {
1281 | // todo: we should only do decodePayload for xhr transports
1282 | var msgs = io.parser.decodePayload(data);
1283 |
1284 | if (msgs && msgs.length) {
1285 | for (var i = 0, l = msgs.length; i < l; i++) {
1286 | this.onPacket(msgs[i]);
1287 | }
1288 | }
1289 | }
1290 |
1291 | return this;
1292 | };
1293 |
1294 | /**
1295 | * Handles packets.
1296 | *
1297 | * @api private
1298 | */
1299 |
1300 | Transport.prototype.onPacket = function (packet) {
1301 | if (packet.type == 'heartbeat') {
1302 | return this.onHeartbeat();
1303 | }
1304 |
1305 | if (packet.type == 'connect' && packet.endpoint == '') {
1306 | this.onConnect();
1307 | }
1308 |
1309 | this.socket.onPacket(packet);
1310 |
1311 | return this;
1312 | };
1313 |
1314 | /**
1315 | * Sets close timeout
1316 | *
1317 | * @api private
1318 | */
1319 |
1320 | Transport.prototype.setCloseTimeout = function () {
1321 | if (!this.closeTimeout) {
1322 | var self = this;
1323 |
1324 | this.closeTimeout = setTimeout(function () {
1325 | self.onDisconnect();
1326 | }, this.socket.closeTimeout);
1327 | }
1328 | };
1329 |
1330 | /**
1331 | * Called when transport disconnects.
1332 | *
1333 | * @api private
1334 | */
1335 |
1336 | Transport.prototype.onDisconnect = function () {
1337 | if (this.close && this.open) this.close();
1338 | this.clearTimeouts();
1339 | this.socket.onDisconnect();
1340 | return this;
1341 | };
1342 |
1343 | /**
1344 | * Called when transport connects
1345 | *
1346 | * @api private
1347 | */
1348 |
1349 | Transport.prototype.onConnect = function () {
1350 | this.socket.onConnect();
1351 | return this;
1352 | }
1353 |
1354 | /**
1355 | * Clears close timeout
1356 | *
1357 | * @api private
1358 | */
1359 |
1360 | Transport.prototype.clearCloseTimeout = function () {
1361 | if (this.closeTimeout) {
1362 | clearTimeout(this.closeTimeout);
1363 | this.closeTimeout = null;
1364 | }
1365 | };
1366 |
1367 | /**
1368 | * Clear timeouts
1369 | *
1370 | * @api private
1371 | */
1372 |
1373 | Transport.prototype.clearTimeouts = function () {
1374 | this.clearCloseTimeout();
1375 |
1376 | if (this.reopenTimeout) {
1377 | clearTimeout(this.reopenTimeout);
1378 | }
1379 | };
1380 |
1381 | /**
1382 | * Sends a packet
1383 | *
1384 | * @param {Object} packet object.
1385 | * @api private
1386 | */
1387 |
1388 | Transport.prototype.packet = function (packet) {
1389 | this.send(io.parser.encodePacket(packet));
1390 | };
1391 |
1392 | /**
1393 | * Send the received heartbeat message back to server. So the server
1394 | * knows we are still connected.
1395 | *
1396 | * @param {String} heartbeat Heartbeat response from the server.
1397 | * @api private
1398 | */
1399 |
1400 | Transport.prototype.onHeartbeat = function (heartbeat) {
1401 | this.packet({ type: 'heartbeat' });
1402 | };
1403 |
1404 | /**
1405 | * Called when the transport opens.
1406 | *
1407 | * @api private
1408 | */
1409 |
1410 | Transport.prototype.onOpen = function () {
1411 | this.open = true;
1412 | this.clearCloseTimeout();
1413 | this.socket.onOpen();
1414 | };
1415 |
1416 | /**
1417 | * Notifies the base when the connection with the Socket.IO server
1418 | * has been disconnected.
1419 | *
1420 | * @api private
1421 | */
1422 |
1423 | Transport.prototype.onClose = function () {
1424 | var self = this;
1425 |
1426 | /* FIXME: reopen delay causing a infinit loop
1427 | this.reopenTimeout = setTimeout(function () {
1428 | self.open();
1429 | }, this.socket.options['reopen delay']);*/
1430 |
1431 | this.open = false;
1432 | this.socket.onClose();
1433 | this.onDisconnect();
1434 | };
1435 |
1436 | /**
1437 | * Generates a connection url based on the Socket.IO URL Protocol.
1438 | * See for more details.
1439 | *
1440 | * @returns {String} Connection url
1441 | * @api private
1442 | */
1443 |
1444 | Transport.prototype.prepareUrl = function () {
1445 | var options = this.socket.options;
1446 |
1447 | return this.scheme() + '://'
1448 | + options.host + ':' + options.port + '/'
1449 | + options.resource + '/' + io.protocol
1450 | + '/' + this.name + '/' + this.sessid;
1451 | };
1452 |
1453 | /**
1454 | * Checks if the transport is ready to start a connection.
1455 | *
1456 | * @param {Socket} socket The socket instance that needs a transport
1457 | * @param {Function} fn The callback
1458 | * @api private
1459 | */
1460 |
1461 | Transport.prototype.ready = function (socket, fn) {
1462 | fn.call(this);
1463 | };
1464 | })(
1465 | 'undefined' != typeof io ? io : module.exports
1466 | , 'undefined' != typeof io ? io : module.parent.exports
1467 | );
1468 |
1469 | /**
1470 | * socket.io
1471 | * Copyright(c) 2011 LearnBoost
1472 | * MIT Licensed
1473 | */
1474 |
1475 | (function (exports, io, global) {
1476 |
1477 | /**
1478 | * Expose constructor.
1479 | */
1480 |
1481 | exports.Socket = Socket;
1482 |
1483 | /**
1484 | * Create a new `Socket.IO client` which can establish a persistent
1485 | * connection with a Socket.IO enabled server.
1486 | *
1487 | * @api public
1488 | */
1489 |
1490 | function Socket (options) {
1491 | this.options = {
1492 | port: 80
1493 | , secure: false
1494 | , document: 'document' in global ? document : false
1495 | , resource: 'socket.io'
1496 | , transports: io.transports
1497 | , 'connect timeout': 10000
1498 | , 'try multiple transports': true
1499 | , 'reconnect': true
1500 | , 'reconnection delay': 500
1501 | , 'reconnection limit': Infinity
1502 | , 'reopen delay': 3000
1503 | , 'max reconnection attempts': 10
1504 | , 'sync disconnect on unload': true
1505 | , 'auto connect': true
1506 | , 'flash policy port': 10843
1507 | };
1508 |
1509 | io.util.merge(this.options, options);
1510 |
1511 | this.connected = false;
1512 | this.open = false;
1513 | this.connecting = false;
1514 | this.reconnecting = false;
1515 | this.namespaces = {};
1516 | this.buffer = [];
1517 | this.doBuffer = false;
1518 |
1519 | if (this.options['sync disconnect on unload'] &&
1520 | (!this.isXDomain() || io.util.ua.hasCORS)) {
1521 | var self = this;
1522 |
1523 | io.util.on(global, 'beforeunload', function () {
1524 | self.disconnectSync();
1525 | }, false);
1526 | }
1527 |
1528 | if (this.options['auto connect']) {
1529 | this.connect();
1530 | }
1531 | };
1532 |
1533 | /**
1534 | * Apply EventEmitter mixin.
1535 | */
1536 |
1537 | io.util.mixin(Socket, io.EventEmitter);
1538 |
1539 | /**
1540 | * Returns a namespace listener/emitter for this socket
1541 | *
1542 | * @api public
1543 | */
1544 |
1545 | Socket.prototype.of = function (name) {
1546 | if (!this.namespaces[name]) {
1547 | this.namespaces[name] = new io.SocketNamespace(this, name);
1548 |
1549 | if (name !== '') {
1550 | this.namespaces[name].packet({ type: 'connect' });
1551 | }
1552 | }
1553 |
1554 | return this.namespaces[name];
1555 | };
1556 |
1557 | /**
1558 | * Emits the given event to the Socket and all namespaces
1559 | *
1560 | * @api private
1561 | */
1562 |
1563 | Socket.prototype.publish = function () {
1564 | this.emit.apply(this, arguments);
1565 |
1566 | var nsp;
1567 |
1568 | for (var i in this.namespaces) {
1569 | if (this.namespaces.hasOwnProperty(i)) {
1570 | nsp = this.of(i);
1571 | nsp.$emit.apply(nsp, arguments);
1572 | }
1573 | }
1574 | };
1575 |
1576 | /**
1577 | * Performs the handshake
1578 | *
1579 | * @api private
1580 | */
1581 |
1582 | function empty () { };
1583 |
1584 | Socket.prototype.handshake = function (fn) {
1585 | var self = this
1586 | , options = this.options;
1587 |
1588 | function complete (data) {
1589 | if (data instanceof Error) {
1590 | self.onError(data.message);
1591 | } else {
1592 | fn.apply(null, data.split(':'));
1593 | }
1594 | };
1595 |
1596 | var url = [
1597 | 'http' + (options.secure ? 's' : '') + ':/'
1598 | , options.host + ':' + options.port
1599 | , options.resource
1600 | , io.protocol
1601 | , io.util.query(this.options.query, 't=' + +new Date)
1602 | ].join('/');
1603 |
1604 | if (this.isXDomain() && !io.util.ua.hasCORS) {
1605 | var insertAt = document.getElementsByTagName('script')[0]
1606 | , script = document.createElement('script');
1607 |
1608 | script.src = url + '&jsonp=' + io.j.length;
1609 | insertAt.parentNode.insertBefore(script, insertAt);
1610 |
1611 | io.j.push(function (data) {
1612 | complete(data);
1613 | script.parentNode.removeChild(script);
1614 | });
1615 | } else {
1616 | var xhr = io.util.request();
1617 |
1618 | xhr.open('GET', url, true);
1619 | xhr.onreadystatechange = function () {
1620 | if (xhr.readyState == 4) {
1621 | xhr.onreadystatechange = empty;
1622 |
1623 | if (xhr.status == 200) {
1624 | complete(xhr.responseText);
1625 | } else {
1626 | !self.reconnecting && self.onError(xhr.responseText);
1627 | }
1628 | }
1629 | };
1630 | xhr.send(null);
1631 | }
1632 | };
1633 |
1634 | /**
1635 | * Find an available transport based on the options supplied in the constructor.
1636 | *
1637 | * @api private
1638 | */
1639 |
1640 | Socket.prototype.getTransport = function (override) {
1641 | var transports = override || this.transports, match;
1642 |
1643 | for (var i = 0, transport; transport = transports[i]; i++) {
1644 | if (io.Transport[transport]
1645 | && io.Transport[transport].check(this)
1646 | && (!this.isXDomain() || io.Transport[transport].xdomainCheck())) {
1647 | return new io.Transport[transport](this, this.sessionid);
1648 | }
1649 | }
1650 |
1651 | return null;
1652 | };
1653 |
1654 | /**
1655 | * Connects to the server.
1656 | *
1657 | * @param {Function} [fn] Callback.
1658 | * @returns {io.Socket}
1659 | * @api public
1660 | */
1661 |
1662 | Socket.prototype.connect = function (fn) {
1663 | if (this.connecting) {
1664 | return this;
1665 | }
1666 |
1667 | var self = this;
1668 |
1669 | this.handshake(function (sid, heartbeat, close, transports) {
1670 | self.sessionid = sid;
1671 | self.closeTimeout = close * 1000;
1672 | self.heartbeatTimeout = heartbeat * 1000;
1673 | self.transports = io.util.intersect(
1674 | transports.split(',')
1675 | , self.options.transports
1676 | );
1677 |
1678 | function connect (transports){
1679 | if (self.transport) self.transport.clearTimeouts();
1680 |
1681 | self.transport = self.getTransport(transports);
1682 | if (!self.transport) return self.publish('connect_failed');
1683 |
1684 | // once the transport is ready
1685 | self.transport.ready(self, function () {
1686 | self.connecting = true;
1687 | self.publish('connecting', self.transport.name);
1688 | self.transport.open();
1689 |
1690 | if (self.options['connect timeout']) {
1691 | self.connectTimeoutTimer = setTimeout(function () {
1692 | if (!self.connected) {
1693 | self.connecting = false;
1694 |
1695 | if (self.options['try multiple transports']) {
1696 | if (!self.remainingTransports) {
1697 | self.remainingTransports = self.transports.slice(0);
1698 | }
1699 |
1700 | var remaining = self.remainingTransports;
1701 |
1702 | while (remaining.length > 0 && remaining.splice(0,1)[0] !=
1703 | self.transport.name) {}
1704 |
1705 | if (remaining.length){
1706 | connect(remaining);
1707 | } else {
1708 | self.publish('connect_failed');
1709 | }
1710 | }
1711 | }
1712 | }, self.options['connect timeout']);
1713 | }
1714 | });
1715 | }
1716 |
1717 | connect();
1718 |
1719 | self.once('connect', function (){
1720 | clearTimeout(self.connectTimeoutTimer);
1721 |
1722 | fn && typeof fn == 'function' && fn();
1723 | });
1724 | });
1725 |
1726 | return this;
1727 | };
1728 |
1729 | /**
1730 | * Sends a message.
1731 | *
1732 | * @param {Object} data packet.
1733 | * @returns {io.Socket}
1734 | * @api public
1735 | */
1736 |
1737 | Socket.prototype.packet = function (data) {
1738 | if (this.connected && !this.doBuffer) {
1739 | this.transport.packet(data);
1740 | } else {
1741 | this.buffer.push(data);
1742 | }
1743 |
1744 | return this;
1745 | };
1746 |
1747 | /**
1748 | * Sets buffer state
1749 | *
1750 | * @api private
1751 | */
1752 |
1753 | Socket.prototype.setBuffer = function (v) {
1754 | this.doBuffer = v;
1755 |
1756 | if (!v && this.connected && this.buffer.length) {
1757 | this.transport.payload(this.buffer);
1758 | this.buffer = [];
1759 | }
1760 | };
1761 |
1762 | /**
1763 | * Disconnect the established connect.
1764 | *
1765 | * @returns {io.Socket}
1766 | * @api public
1767 | */
1768 |
1769 | Socket.prototype.disconnect = function () {
1770 | if (this.connected) {
1771 | if (this.open) {
1772 | this.of('').packet({ type: 'disconnect' });
1773 | }
1774 |
1775 | // handle disconnection immediately
1776 | this.onDisconnect('booted');
1777 | }
1778 |
1779 | return this;
1780 | };
1781 |
1782 | /**
1783 | * Disconnects the socket with a sync XHR.
1784 | *
1785 | * @api private
1786 | */
1787 |
1788 | Socket.prototype.disconnectSync = function () {
1789 | // ensure disconnection
1790 | var xhr = io.util.request()
1791 | , uri = this.resource + '/' + io.protocol + '/' + this.sessionid;
1792 |
1793 | xhr.open('GET', uri, true);
1794 |
1795 | // handle disconnection immediately
1796 | this.onDisconnect('booted');
1797 | };
1798 |
1799 | /**
1800 | * Check if we need to use cross domain enabled transports. Cross domain would
1801 | * be a different port or different domain name.
1802 | *
1803 | * @returns {Boolean}
1804 | * @api private
1805 | */
1806 |
1807 | Socket.prototype.isXDomain = function () {
1808 |
1809 | var port = global.location.port ||
1810 | ('https:' == global.location.protocol ? 443 : 80);
1811 |
1812 | return this.options.host !== global.location.hostname
1813 | || this.options.port != port;
1814 | };
1815 |
1816 | /**
1817 | * Called upon handshake.
1818 | *
1819 | * @api private
1820 | */
1821 |
1822 | Socket.prototype.onConnect = function () {
1823 | if (!this.connected) {
1824 | this.connected = true;
1825 | this.connecting = false;
1826 | if (!this.doBuffer) {
1827 | // make sure to flush the buffer
1828 | this.setBuffer(false);
1829 | }
1830 | this.emit('connect');
1831 | }
1832 | };
1833 |
1834 | /**
1835 | * Called when the transport opens
1836 | *
1837 | * @api private
1838 | */
1839 |
1840 | Socket.prototype.onOpen = function () {
1841 | this.open = true;
1842 | };
1843 |
1844 | /**
1845 | * Called when the transport closes.
1846 | *
1847 | * @api private
1848 | */
1849 |
1850 | Socket.prototype.onClose = function () {
1851 | this.open = false;
1852 | };
1853 |
1854 | /**
1855 | * Called when the transport first opens a connection
1856 | *
1857 | * @param text
1858 | */
1859 |
1860 | Socket.prototype.onPacket = function (packet) {
1861 | this.of(packet.endpoint).onPacket(packet);
1862 | };
1863 |
1864 | /**
1865 | * Handles an error.
1866 | *
1867 | * @api private
1868 | */
1869 |
1870 | Socket.prototype.onError = function (err) {
1871 | if (err && err.advice) {
1872 | if (err.advice === 'reconnect' && this.connected) {
1873 | this.disconnect();
1874 | this.reconnect();
1875 | }
1876 | }
1877 |
1878 | this.publish('error', err && err.reason ? err.reason : err);
1879 | };
1880 |
1881 | /**
1882 | * Called when the transport disconnects.
1883 | *
1884 | * @api private
1885 | */
1886 |
1887 | Socket.prototype.onDisconnect = function (reason) {
1888 | var wasConnected = this.connected;
1889 |
1890 | this.connected = false;
1891 | this.connecting = false;
1892 | this.open = false;
1893 |
1894 | if (wasConnected) {
1895 | this.transport.close();
1896 | this.transport.clearTimeouts();
1897 | this.publish('disconnect', reason);
1898 |
1899 | if ('booted' != reason && this.options.reconnect && !this.reconnecting) {
1900 | this.reconnect();
1901 | }
1902 | }
1903 | };
1904 |
1905 | /**
1906 | * Called upon reconnection.
1907 | *
1908 | * @api private
1909 | */
1910 |
1911 | Socket.prototype.reconnect = function () {
1912 | this.reconnecting = true;
1913 | this.reconnectionAttempts = 0;
1914 | this.reconnectionDelay = this.options['reconnection delay'];
1915 |
1916 | var self = this
1917 | , maxAttempts = this.options['max reconnection attempts']
1918 | , tryMultiple = this.options['try multiple transports']
1919 | , limit = this.options['reconnection limit'];
1920 |
1921 | function reset () {
1922 | if (self.connected) {
1923 | for (var i in self.namespaces) {
1924 | if (self.namespaces.hasOwnProperty(i) && '' !== i) {
1925 | self.namespaces[i].packet({ type: 'connect' });
1926 | }
1927 | }
1928 | self.publish('reconnect', self.transport.name, self.reconnectionAttempts);
1929 | }
1930 |
1931 | self.removeListener('connect_failed', maybeReconnect);
1932 | self.removeListener('connect', maybeReconnect);
1933 |
1934 | self.reconnecting = false;
1935 |
1936 | delete self.reconnectionAttempts;
1937 | delete self.reconnectionDelay;
1938 | delete self.reconnectionTimer;
1939 | delete self.redoTransports;
1940 |
1941 | self.options['try multiple transports'] = tryMultiple;
1942 | };
1943 |
1944 | function maybeReconnect () {
1945 | if (!self.reconnecting) {
1946 | return;
1947 | }
1948 |
1949 | if (self.connected) {
1950 | return reset();
1951 | };
1952 |
1953 | if (self.connecting && self.reconnecting) {
1954 | return self.reconnectionTimer = setTimeout(maybeReconnect, 1000);
1955 | }
1956 |
1957 | if (self.reconnectionAttempts++ >= maxAttempts) {
1958 | if (!self.redoTransports) {
1959 | self.on('connect_failed', maybeReconnect);
1960 | self.options['try multiple transports'] = true;
1961 | self.transport = self.getTransport();
1962 | self.redoTransports = true;
1963 | self.connect();
1964 | } else {
1965 | self.publish('reconnect_failed');
1966 | reset();
1967 | }
1968 | } else {
1969 | if (self.reconnectionDelay < limit) {
1970 | self.reconnectionDelay *= 2; // exponential back off
1971 | }
1972 |
1973 | self.connect();
1974 | self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts);
1975 | self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay);
1976 | }
1977 | };
1978 |
1979 | this.options['try multiple transports'] = false;
1980 | this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay);
1981 |
1982 | this.on('connect', maybeReconnect);
1983 | };
1984 |
1985 | })(
1986 | 'undefined' != typeof io ? io : module.exports
1987 | , 'undefined' != typeof io ? io : module.parent.exports
1988 | , this
1989 | );
1990 | /**
1991 | * socket.io
1992 | * Copyright(c) 2011 LearnBoost
1993 | * MIT Licensed
1994 | */
1995 |
1996 | (function (exports, io) {
1997 |
1998 | /**
1999 | * Expose constructor.
2000 | */
2001 |
2002 | exports.SocketNamespace = SocketNamespace;
2003 |
2004 | /**
2005 | * Socket namespace constructor.
2006 | *
2007 | * @constructor
2008 | * @api public
2009 | */
2010 |
2011 | function SocketNamespace (socket, name) {
2012 | this.socket = socket;
2013 | this.name = name || '';
2014 | this.flags = {};
2015 | this.json = new Flag(this, 'json');
2016 | this.ackPackets = 0;
2017 | this.acks = {};
2018 | };
2019 |
2020 | /**
2021 | * Apply EventEmitter mixin.
2022 | */
2023 |
2024 | io.util.mixin(SocketNamespace, io.EventEmitter);
2025 |
2026 | /**
2027 | * Copies emit since we override it
2028 | *
2029 | * @api private
2030 | */
2031 |
2032 | SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit;
2033 |
2034 | /**
2035 | * Creates a new namespace, by proxying the request to the socket. This
2036 | * allows us to use the synax as we do on the server.
2037 | *
2038 | * @api public
2039 | */
2040 |
2041 | SocketNamespace.prototype.of = function () {
2042 | return this.socket.of.apply(this.socket, arguments);
2043 | };
2044 |
2045 | /**
2046 | * Sends a packet.
2047 | *
2048 | * @api private
2049 | */
2050 |
2051 | SocketNamespace.prototype.packet = function (packet) {
2052 | packet.endpoint = this.name;
2053 | this.socket.packet(packet);
2054 | this.flags = {};
2055 | return this;
2056 | };
2057 |
2058 | /**
2059 | * Sends a message
2060 | *
2061 | * @api public
2062 | */
2063 |
2064 | SocketNamespace.prototype.send = function (data, fn) {
2065 | var packet = {
2066 | type: this.flags.json ? 'json' : 'message'
2067 | , data: data
2068 | };
2069 |
2070 | if ('function' == typeof fn) {
2071 | packet.id = ++this.ackPackets;
2072 | packet.ack = true;
2073 | this.acks[packet.id] = fn;
2074 | }
2075 |
2076 | return this.packet(packet);
2077 | };
2078 |
2079 | /**
2080 | * Emits an event
2081 | *
2082 | * @api public
2083 | */
2084 |
2085 | SocketNamespace.prototype.emit = function (name) {
2086 | var args = Array.prototype.slice.call(arguments, 1)
2087 | , lastArg = args[args.length - 1]
2088 | , packet = {
2089 | type: 'event'
2090 | , name: name
2091 | };
2092 |
2093 | if ('function' == typeof lastArg) {
2094 | packet.id = ++this.ackPackets;
2095 | packet.ack = 'data';
2096 | this.acks[packet.id] = lastArg;
2097 | args = args.slice(0, args.length - 1);
2098 | }
2099 |
2100 | packet.args = args;
2101 |
2102 | return this.packet(packet);
2103 | };
2104 |
2105 | /**
2106 | * Disconnects the namespace
2107 | *
2108 | * @api private
2109 | */
2110 |
2111 | SocketNamespace.prototype.disconnect = function () {
2112 | if (this.name === '') {
2113 | this.socket.disconnect();
2114 | } else {
2115 | this.packet({ type: 'disconnect' });
2116 | this.$emit('disconnect');
2117 | }
2118 |
2119 | return this;
2120 | };
2121 |
2122 | /**
2123 | * Handles a packet
2124 | *
2125 | * @api private
2126 | */
2127 |
2128 | SocketNamespace.prototype.onPacket = function (packet) {
2129 | var self = this;
2130 |
2131 | function ack () {
2132 | self.packet({
2133 | type: 'ack'
2134 | , args: io.util.toArray(arguments)
2135 | , ackId: packet.id
2136 | });
2137 | };
2138 |
2139 | switch (packet.type) {
2140 | case 'connect':
2141 | this.$emit('connect');
2142 | break;
2143 |
2144 | case 'disconnect':
2145 | if (this.name === '') {
2146 | this.socket.onDisconnect(packet.reason || 'booted');
2147 | } else {
2148 | this.$emit('disconnect', packet.reason);
2149 | }
2150 | break;
2151 |
2152 | case 'message':
2153 | case 'json':
2154 | var params = ['message', packet.data];
2155 |
2156 | if (packet.ack == 'data') {
2157 | params.push(ack);
2158 | } else if (packet.ack) {
2159 | this.packet({ type: 'ack', ackId: packet.id });
2160 | }
2161 |
2162 | this.$emit.apply(this, params);
2163 | break;
2164 |
2165 | case 'event':
2166 | var params = [packet.name].concat(packet.args);
2167 |
2168 | if (packet.ack == 'data')
2169 | params.push(ack);
2170 |
2171 | this.$emit.apply(this, params);
2172 | break;
2173 |
2174 | case 'ack':
2175 | if (this.acks[packet.ackId]) {
2176 | this.acks[packet.ackId].apply(this, packet.args);
2177 | delete this.acks[packet.ackId];
2178 | }
2179 | break;
2180 |
2181 | case 'error':
2182 | if (packet.advice){
2183 | this.socket.onError(packet);
2184 | } else {
2185 | if (packet.reason == 'unauthorized') {
2186 | this.$emit('connect_failed', packet.reason);
2187 | } else {
2188 | this.$emit('error', packet.reason);
2189 | }
2190 | }
2191 | break;
2192 | }
2193 | };
2194 |
2195 | /**
2196 | * Flag interface.
2197 | *
2198 | * @api private
2199 | */
2200 |
2201 | function Flag (nsp, name) {
2202 | this.namespace = nsp;
2203 | this.name = name;
2204 | };
2205 |
2206 | /**
2207 | * Send a message
2208 | *
2209 | * @api public
2210 | */
2211 |
2212 | Flag.prototype.send = function () {
2213 | this.namespace.flags[this.name] = true;
2214 | this.namespace.send.apply(this.namespace, arguments);
2215 | };
2216 |
2217 | /**
2218 | * Emit an event
2219 | *
2220 | * @api public
2221 | */
2222 |
2223 | Flag.prototype.emit = function () {
2224 | this.namespace.flags[this.name] = true;
2225 | this.namespace.emit.apply(this.namespace, arguments);
2226 | };
2227 |
2228 | })(
2229 | 'undefined' != typeof io ? io : module.exports
2230 | , 'undefined' != typeof io ? io : module.parent.exports
2231 | );
2232 |
2233 | /**
2234 | * socket.io
2235 | * Copyright(c) 2011 LearnBoost
2236 | * MIT Licensed
2237 | */
2238 |
2239 | (function (exports, io, global) {
2240 |
2241 | /**
2242 | * Expose constructor.
2243 | */
2244 |
2245 | exports.websocket = WS;
2246 |
2247 | /**
2248 | * The WebSocket transport uses the HTML5 WebSocket API to establish an
2249 | * persistent connection with the Socket.IO server. This transport will also
2250 | * be inherited by the FlashSocket fallback as it provides a API compatible
2251 | * polyfill for the WebSockets.
2252 | *
2253 | * @constructor
2254 | * @extends {io.Transport}
2255 | * @api public
2256 | */
2257 |
2258 | function WS (socket) {
2259 | io.Transport.apply(this, arguments);
2260 | };
2261 |
2262 | /**
2263 | * Inherits from Transport.
2264 | */
2265 |
2266 | io.util.inherit(WS, io.Transport);
2267 |
2268 | /**
2269 | * Transport name
2270 | *
2271 | * @api public
2272 | */
2273 |
2274 | WS.prototype.name = 'websocket';
2275 |
2276 | /**
2277 | * Initializes a new `WebSocket` connection with the Socket.IO server. We attach
2278 | * all the appropriate listeners to handle the responses from the server.
2279 | *
2280 | * @returns {Transport}
2281 | * @api public
2282 | */
2283 |
2284 | WS.prototype.open = function () {
2285 | var query = io.util.query(this.socket.options.query)
2286 | , self = this
2287 | , Socket
2288 |
2289 |
2290 | if (!Socket) {
2291 | Socket = global.MozWebSocket || global.WebSocket;
2292 | }
2293 |
2294 | this.websocket = new Socket(this.prepareUrl() + query);
2295 |
2296 | this.websocket.onopen = function () {
2297 | self.onOpen();
2298 | self.socket.setBuffer(false);
2299 | };
2300 | this.websocket.onmessage = function (ev) {
2301 | self.onData(ev.data);
2302 | };
2303 | this.websocket.onclose = function () {
2304 | self.onClose();
2305 | self.socket.setBuffer(true);
2306 | };
2307 | this.websocket.onerror = function (e) {
2308 | self.onError(e);
2309 | };
2310 |
2311 | return this;
2312 | };
2313 |
2314 | /**
2315 | * Send a message to the Socket.IO server. The message will automatically be
2316 | * encoded in the correct message format.
2317 | *
2318 | * @returns {Transport}
2319 | * @api public
2320 | */
2321 |
2322 | WS.prototype.send = function (data) {
2323 | this.websocket.send(data);
2324 | return this;
2325 | };
2326 |
2327 | /**
2328 | * Payload
2329 | *
2330 | * @api private
2331 | */
2332 |
2333 | WS.prototype.payload = function (arr) {
2334 | for (var i = 0, l = arr.length; i < l; i++) {
2335 | this.packet(arr[i]);
2336 | }
2337 | return this;
2338 | };
2339 |
2340 | /**
2341 | * Disconnect the established `WebSocket` connection.
2342 | *
2343 | * @returns {Transport}
2344 | * @api public
2345 | */
2346 |
2347 | WS.prototype.close = function () {
2348 | this.websocket.close();
2349 | return this;
2350 | };
2351 |
2352 | /**
2353 | * Handle the errors that `WebSocket` might be giving when we
2354 | * are attempting to connect or send messages.
2355 | *
2356 | * @param {Error} e The error.
2357 | * @api private
2358 | */
2359 |
2360 | WS.prototype.onError = function (e) {
2361 | this.socket.onError(e);
2362 | };
2363 |
2364 | /**
2365 | * Returns the appropriate scheme for the URI generation.
2366 | *
2367 | * @api private
2368 | */
2369 | WS.prototype.scheme = function () {
2370 | return this.socket.options.secure ? 'wss' : 'ws';
2371 | };
2372 |
2373 | /**
2374 | * Checks if the browser has support for native `WebSockets` and that
2375 | * it's not the polyfill created for the FlashSocket transport.
2376 | *
2377 | * @return {Boolean}
2378 | * @api public
2379 | */
2380 |
2381 | WS.check = function () {
2382 | return ('WebSocket' in global && !('__addTask' in WebSocket))
2383 | || 'MozWebSocket' in global;
2384 | };
2385 |
2386 | /**
2387 | * Check if the `WebSocket` transport support cross domain communications.
2388 | *
2389 | * @returns {Boolean}
2390 | * @api public
2391 | */
2392 |
2393 | WS.xdomainCheck = function () {
2394 | return true;
2395 | };
2396 |
2397 | /**
2398 | * Add the transport to your public io.transports array.
2399 | *
2400 | * @api private
2401 | */
2402 |
2403 | io.transports.push('websocket');
2404 |
2405 | })(
2406 | 'undefined' != typeof io ? io.Transport : module.exports
2407 | , 'undefined' != typeof io ? io : module.parent.exports
2408 | , this
2409 | );
2410 |
2411 | /**
2412 | * socket.io
2413 | * Copyright(c) 2011 LearnBoost
2414 | * MIT Licensed
2415 | */
2416 |
2417 | (function (exports, io, global) {
2418 |
2419 | /**
2420 | * Expose constructor.
2421 | *
2422 | * @api public
2423 | */
2424 |
2425 | exports.XHR = XHR;
2426 |
2427 | /**
2428 | * XHR constructor
2429 | *
2430 | * @costructor
2431 | * @api public
2432 | */
2433 |
2434 | function XHR (socket) {
2435 | if (!socket) return;
2436 |
2437 | io.Transport.apply(this, arguments);
2438 | this.sendBuffer = [];
2439 | };
2440 |
2441 | /**
2442 | * Inherits from Transport.
2443 | */
2444 |
2445 | io.util.inherit(XHR, io.Transport);
2446 |
2447 | /**
2448 | * Establish a connection
2449 | *
2450 | * @returns {Transport}
2451 | * @api public
2452 | */
2453 |
2454 | XHR.prototype.open = function () {
2455 | this.socket.setBuffer(false);
2456 | this.onOpen();
2457 | this.get();
2458 |
2459 | // we need to make sure the request succeeds since we have no indication
2460 | // whether the request opened or not until it succeeded.
2461 | this.setCloseTimeout();
2462 |
2463 | return this;
2464 | };
2465 |
2466 | /**
2467 | * Check if we need to send data to the Socket.IO server, if we have data in our
2468 | * buffer we encode it and forward it to the `post` method.
2469 | *
2470 | * @api private
2471 | */
2472 |
2473 | XHR.prototype.payload = function (payload) {
2474 | var msgs = [];
2475 |
2476 | for (var i = 0, l = payload.length; i < l; i++) {
2477 | msgs.push(io.parser.encodePacket(payload[i]));
2478 | }
2479 |
2480 | this.send(io.parser.encodePayload(msgs));
2481 | };
2482 |
2483 | /**
2484 | * Send data to the Socket.IO server.
2485 | *
2486 | * @param data The message
2487 | * @returns {Transport}
2488 | * @api public
2489 | */
2490 |
2491 | XHR.prototype.send = function (data) {
2492 | this.post(data);
2493 | return this;
2494 | };
2495 |
2496 | /**
2497 | * Posts a encoded message to the Socket.IO server.
2498 | *
2499 | * @param {String} data A encoded message.
2500 | * @api private
2501 | */
2502 |
2503 | function empty () { };
2504 |
2505 | XHR.prototype.post = function (data) {
2506 | var self = this;
2507 | this.socket.setBuffer(true);
2508 |
2509 | function stateChange () {
2510 | if (this.readyState == 4) {
2511 | this.onreadystatechange = empty;
2512 | self.posting = false;
2513 |
2514 | if (this.status == 200){
2515 | self.socket.setBuffer(false);
2516 | } else {
2517 | self.onClose();
2518 | }
2519 | }
2520 | }
2521 |
2522 | function onload () {
2523 | this.onload = empty;
2524 | self.socket.setBuffer(false);
2525 | };
2526 |
2527 | this.sendXHR = this.request('POST');
2528 |
2529 | if (global.XDomainRequest && this.sendXHR instanceof XDomainRequest) {
2530 | this.sendXHR.onload = this.sendXHR.onerror = onload;
2531 | } else {
2532 | this.sendXHR.onreadystatechange = stateChange;
2533 | }
2534 |
2535 | this.sendXHR.send(data);
2536 | };
2537 |
2538 | /**
2539 | * Disconnects the established `XHR` connection.
2540 | *
2541 | * @returns {Transport}
2542 | * @api public
2543 | */
2544 |
2545 | XHR.prototype.close = function () {
2546 | this.onClose();
2547 | return this;
2548 | };
2549 |
2550 | /**
2551 | * Generates a configured XHR request
2552 | *
2553 | * @param {String} url The url that needs to be requested.
2554 | * @param {String} method The method the request should use.
2555 | * @returns {XMLHttpRequest}
2556 | * @api private
2557 | */
2558 |
2559 | XHR.prototype.request = function (method) {
2560 | var req = io.util.request(this.socket.isXDomain())
2561 | , query = io.util.query(this.socket.options.query, 't=' + +new Date);
2562 |
2563 | req.open(method || 'GET', this.prepareUrl() + query, true);
2564 |
2565 | if (method == 'POST') {
2566 | try {
2567 | if (req.setRequestHeader) {
2568 | req.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
2569 | } else {
2570 | // XDomainRequest
2571 | req.contentType = 'text/plain';
2572 | }
2573 | } catch (e) {}
2574 | }
2575 |
2576 | return req;
2577 | };
2578 |
2579 | /**
2580 | * Returns the scheme to use for the transport URLs.
2581 | *
2582 | * @api private
2583 | */
2584 |
2585 | XHR.prototype.scheme = function () {
2586 | return this.socket.options.secure ? 'https' : 'http';
2587 | };
2588 |
2589 | /**
2590 | * Check if the XHR transports are supported
2591 | *
2592 | * @param {Boolean} xdomain Check if we support cross domain requests.
2593 | * @returns {Boolean}
2594 | * @api public
2595 | */
2596 |
2597 | XHR.check = function (socket, xdomain) {
2598 | try {
2599 | if (io.util.request(xdomain)) {
2600 | return true;
2601 | }
2602 | } catch(e) {}
2603 |
2604 | return false;
2605 | };
2606 |
2607 | /**
2608 | * Check if the XHR transport supports corss domain requests.
2609 | *
2610 | * @returns {Boolean}
2611 | * @api public
2612 | */
2613 |
2614 | XHR.xdomainCheck = function () {
2615 | return XHR.check(null, true);
2616 | };
2617 |
2618 | })(
2619 | 'undefined' != typeof io ? io.Transport : module.exports
2620 | , 'undefined' != typeof io ? io : module.parent.exports
2621 | , this
2622 | );
2623 |
2624 | /**
2625 | * socket.io
2626 | * Copyright(c) 2011 LearnBoost
2627 | * MIT Licensed
2628 | */
2629 |
2630 | (function (exports, io) {
2631 |
2632 | /**
2633 | * Expose constructor.
2634 | */
2635 |
2636 | exports.htmlfile = HTMLFile;
2637 |
2638 | /**
2639 | * The HTMLFile transport creates a `forever iframe` based transport
2640 | * for Internet Explorer. Regular forever iframe implementations will
2641 | * continuously trigger the browsers buzy indicators. If the forever iframe
2642 | * is created inside a `htmlfile` these indicators will not be trigged.
2643 | *
2644 | * @constructor
2645 | * @extends {io.Transport.XHR}
2646 | * @api public
2647 | */
2648 |
2649 | function HTMLFile (socket) {
2650 | io.Transport.XHR.apply(this, arguments);
2651 | };
2652 |
2653 | /**
2654 | * Inherits from XHR transport.
2655 | */
2656 |
2657 | io.util.inherit(HTMLFile, io.Transport.XHR);
2658 |
2659 | /**
2660 | * Transport name
2661 | *
2662 | * @api public
2663 | */
2664 |
2665 | HTMLFile.prototype.name = 'htmlfile';
2666 |
2667 | /**
2668 | * Creates a new ActiveX `htmlfile` with a forever loading iframe
2669 | * that can be used to listen to messages. Inside the generated
2670 | * `htmlfile` a reference will be made to the HTMLFile transport.
2671 | *
2672 | * @api private
2673 | */
2674 |
2675 | HTMLFile.prototype.get = function () {
2676 | this.doc = new ActiveXObject('htmlfile');
2677 | this.doc.open();
2678 | this.doc.write('');
2679 | this.doc.close();
2680 | this.doc.parentWindow.s = this;
2681 |
2682 | var iframeC = this.doc.createElement('div');
2683 | iframeC.className = 'socketio';
2684 |
2685 | this.doc.body.appendChild(iframeC);
2686 | this.iframe = this.doc.createElement('iframe');
2687 |
2688 | iframeC.appendChild(this.iframe);
2689 |
2690 | var self = this
2691 | , query = io.util.query(this.socket.options.query, 't='+ +new Date);
2692 |
2693 | this.iframe.src = this.prepareUrl() + query;
2694 |
2695 | io.util.on(window, 'unload', function () {
2696 | self.destroy();
2697 | });
2698 | };
2699 |
2700 | /**
2701 | * The Socket.IO server will write script tags inside the forever
2702 | * iframe, this function will be used as callback for the incoming
2703 | * information.
2704 | *
2705 | * @param {String} data The message
2706 | * @param {document} doc Reference to the context
2707 | * @api private
2708 | */
2709 |
2710 | HTMLFile.prototype._ = function (data, doc) {
2711 | this.onData(data);
2712 | try {
2713 | var script = doc.getElementsByTagName('script')[0];
2714 | script.parentNode.removeChild(script);
2715 | } catch (e) { }
2716 | };
2717 |
2718 | /**
2719 | * Destroy the established connection, iframe and `htmlfile`.
2720 | * And calls the `CollectGarbage` function of Internet Explorer
2721 | * to release the memory.
2722 | *
2723 | * @api private
2724 | */
2725 |
2726 | HTMLFile.prototype.destroy = function () {
2727 | if (this.iframe){
2728 | try {
2729 | this.iframe.src = 'about:blank';
2730 | } catch(e){}
2731 |
2732 | this.doc = null;
2733 | this.iframe.parentNode.removeChild(this.iframe);
2734 | this.iframe = null;
2735 |
2736 | CollectGarbage();
2737 | }
2738 | };
2739 |
2740 | /**
2741 | * Disconnects the established connection.
2742 | *
2743 | * @returns {Transport} Chaining.
2744 | * @api public
2745 | */
2746 |
2747 | HTMLFile.prototype.close = function () {
2748 | this.destroy();
2749 | return io.Transport.XHR.prototype.close.call(this);
2750 | };
2751 |
2752 | /**
2753 | * Checks if the browser supports this transport. The browser
2754 | * must have an `ActiveXObject` implementation.
2755 | *
2756 | * @return {Boolean}
2757 | * @api public
2758 | */
2759 |
2760 | HTMLFile.check = function () {
2761 | if ('ActiveXObject' in window){
2762 | try {
2763 | var a = new ActiveXObject('htmlfile');
2764 | return a && io.Transport.XHR.check();
2765 | } catch(e){}
2766 | }
2767 | return false;
2768 | };
2769 |
2770 | /**
2771 | * Check if cross domain requests are supported.
2772 | *
2773 | * @returns {Boolean}
2774 | * @api public
2775 | */
2776 |
2777 | HTMLFile.xdomainCheck = function () {
2778 | // we can probably do handling for sub-domains, we should
2779 | // test that it's cross domain but a subdomain here
2780 | return false;
2781 | };
2782 |
2783 | /**
2784 | * Add the transport to your public io.transports array.
2785 | *
2786 | * @api private
2787 | */
2788 |
2789 | io.transports.push('htmlfile');
2790 |
2791 | })(
2792 | 'undefined' != typeof io ? io.Transport : module.exports
2793 | , 'undefined' != typeof io ? io : module.parent.exports
2794 | );
2795 |
2796 | /**
2797 | * socket.io
2798 | * Copyright(c) 2011 LearnBoost
2799 | * MIT Licensed
2800 | */
2801 |
2802 | (function (exports, io, global) {
2803 |
2804 | /**
2805 | * Expose constructor.
2806 | */
2807 |
2808 | exports['xhr-polling'] = XHRPolling;
2809 |
2810 | /**
2811 | * The XHR-polling transport uses long polling XHR requests to create a
2812 | * "persistent" connection with the server.
2813 | *
2814 | * @constructor
2815 | * @api public
2816 | */
2817 |
2818 | function XHRPolling () {
2819 | io.Transport.XHR.apply(this, arguments);
2820 | };
2821 |
2822 | /**
2823 | * Inherits from XHR transport.
2824 | */
2825 |
2826 | io.util.inherit(XHRPolling, io.Transport.XHR);
2827 |
2828 | /**
2829 | * Merge the properties from XHR transport
2830 | */
2831 |
2832 | io.util.merge(XHRPolling, io.Transport.XHR);
2833 |
2834 | /**
2835 | * Transport name
2836 | *
2837 | * @api public
2838 | */
2839 |
2840 | XHRPolling.prototype.name = 'xhr-polling';
2841 |
2842 | /**
2843 | * Establish a connection, for iPhone and Android this will be done once the page
2844 | * is loaded.
2845 | *
2846 | * @returns {Transport} Chaining.
2847 | * @api public
2848 | */
2849 |
2850 | XHRPolling.prototype.open = function () {
2851 | var self = this;
2852 |
2853 | io.Transport.XHR.prototype.open.call(self);
2854 | return false;
2855 | };
2856 |
2857 | /**
2858 | * Starts a XHR request to wait for incoming messages.
2859 | *
2860 | * @api private
2861 | */
2862 |
2863 | function empty () {};
2864 |
2865 | XHRPolling.prototype.get = function () {
2866 | if (!this.open) return;
2867 |
2868 | var self = this;
2869 |
2870 | function stateChange () {
2871 | if (this.readyState == 4) {
2872 | this.onreadystatechange = empty;
2873 |
2874 | if (this.status == 200) {
2875 | self.onData(this.responseText);
2876 | self.get();
2877 | } else {
2878 | self.onClose();
2879 | }
2880 | }
2881 | };
2882 |
2883 | function onload () {
2884 | this.onload = empty;
2885 | self.onData(this.responseText);
2886 | self.get();
2887 | };
2888 |
2889 | this.xhr = this.request();
2890 |
2891 | if (global.XDomainRequest && this.xhr instanceof XDomainRequest) {
2892 | this.xhr.onload = this.xhr.onerror = onload;
2893 | } else {
2894 | this.xhr.onreadystatechange = stateChange;
2895 | }
2896 |
2897 | this.xhr.send(null);
2898 | };
2899 |
2900 | /**
2901 | * Handle the unclean close behavior.
2902 | *
2903 | * @api private
2904 | */
2905 |
2906 | XHRPolling.prototype.onClose = function () {
2907 | io.Transport.XHR.prototype.onClose.call(this);
2908 |
2909 | if (this.xhr) {
2910 | this.xhr.onreadystatechange = this.xhr.onload = empty;
2911 | try {
2912 | this.xhr.abort();
2913 | } catch(e){}
2914 | this.xhr = null;
2915 | }
2916 | };
2917 |
2918 | /**
2919 | * Webkit based browsers show a infinit spinner when you start a XHR request
2920 | * before the browsers onload event is called so we need to defer opening of
2921 | * the transport until the onload event is called. Wrapping the cb in our
2922 | * defer method solve this.
2923 | *
2924 | * @param {Socket} socket The socket instance that needs a transport
2925 | * @param {Function} fn The callback
2926 | * @api private
2927 | */
2928 |
2929 | XHRPolling.prototype.ready = function (socket, fn) {
2930 | var self = this;
2931 |
2932 | io.util.defer(function () {
2933 | fn.call(self);
2934 | });
2935 | };
2936 |
2937 | /**
2938 | * Add the transport to your public io.transports array.
2939 | *
2940 | * @api private
2941 | */
2942 |
2943 | io.transports.push('xhr-polling');
2944 |
2945 | })(
2946 | 'undefined' != typeof io ? io.Transport : module.exports
2947 | , 'undefined' != typeof io ? io : module.parent.exports
2948 | , this
2949 | );
2950 |
2951 | /**
2952 | * socket.io
2953 | * Copyright(c) 2011 LearnBoost
2954 | * MIT Licensed
2955 | */
2956 |
2957 | (function (exports, io, global) {
2958 | /**
2959 | * There is a way to hide the loading indicator in Firefox. If you create and
2960 | * remove a iframe it will stop showing the current loading indicator.
2961 | * Unfortunately we can't feature detect that and UA sniffing is evil.
2962 | *
2963 | * @api private
2964 | */
2965 |
2966 | var indicator = global.document && "MozAppearance" in
2967 | global.document.documentElement.style;
2968 |
2969 | /**
2970 | * Expose constructor.
2971 | */
2972 |
2973 | exports['jsonp-polling'] = JSONPPolling;
2974 |
2975 | /**
2976 | * The JSONP transport creates an persistent connection by dynamically
2977 | * inserting a script tag in the page. This script tag will receive the
2978 | * information of the Socket.IO server. When new information is received
2979 | * it creates a new script tag for the new data stream.
2980 | *
2981 | * @constructor
2982 | * @extends {io.Transport.xhr-polling}
2983 | * @api public
2984 | */
2985 |
2986 | function JSONPPolling (socket) {
2987 | io.Transport['xhr-polling'].apply(this, arguments);
2988 |
2989 | this.index = io.j.length;
2990 |
2991 | var self = this;
2992 |
2993 | io.j.push(function (msg) {
2994 | self._(msg);
2995 | });
2996 | };
2997 |
2998 | /**
2999 | * Inherits from XHR polling transport.
3000 | */
3001 |
3002 | io.util.inherit(JSONPPolling, io.Transport['xhr-polling']);
3003 |
3004 | /**
3005 | * Transport name
3006 | *
3007 | * @api public
3008 | */
3009 |
3010 | JSONPPolling.prototype.name = 'jsonp-polling';
3011 |
3012 | /**
3013 | * Posts a encoded message to the Socket.IO server using an iframe.
3014 | * The iframe is used because script tags can create POST based requests.
3015 | * The iframe is positioned outside of the view so the user does not
3016 | * notice it's existence.
3017 | *
3018 | * @param {String} data A encoded message.
3019 | * @api private
3020 | */
3021 |
3022 | JSONPPolling.prototype.post = function (data) {
3023 | var self = this
3024 | , query = io.util.query(
3025 | this.socket.options.query
3026 | , 't='+ (+new Date) + '&i=' + this.index
3027 | );
3028 |
3029 | if (!this.form) {
3030 | var form = document.createElement('form')
3031 | , area = document.createElement('textarea')
3032 | , id = this.iframeId = 'socketio_iframe_' + this.index
3033 | , iframe;
3034 |
3035 | form.className = 'socketio';
3036 | form.style.position = 'absolute';
3037 | form.style.top = '-1000px';
3038 | form.style.left = '-1000px';
3039 | form.target = id;
3040 | form.method = 'POST';
3041 | form.setAttribute('accept-charset', 'utf-8');
3042 | area.name = 'd';
3043 | form.appendChild(area);
3044 | document.body.appendChild(form);
3045 |
3046 | this.form = form;
3047 | this.area = area;
3048 | }
3049 |
3050 | this.form.action = this.prepareUrl() + query;
3051 |
3052 | function complete () {
3053 | initIframe();
3054 | self.socket.setBuffer(false);
3055 | };
3056 |
3057 | function initIframe () {
3058 | if (self.iframe) {
3059 | self.form.removeChild(self.iframe);
3060 | }
3061 |
3062 | try {
3063 | // ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
3064 | iframe = document.createElement('