├── .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('