firehose 0.1.1 → 0.2.alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/.env.sample +10 -0
  2. data/.gitignore +2 -0
  3. data/Procfile +1 -1
  4. data/README.md +117 -11
  5. data/config/rainbows.rb +20 -0
  6. data/firehose.gemspec +9 -6
  7. data/lib/assets/flash/firehose/WebSocketMain.swf +0 -0
  8. data/lib/assets/javascripts/firehose.js.coffee +4 -1
  9. data/lib/assets/javascripts/firehose/consumer.js.coffee +3 -11
  10. data/lib/assets/javascripts/firehose/lib/jquery.cors.headers.js.coffee +16 -0
  11. data/lib/assets/javascripts/firehose/lib/swfobject.js +4 -0
  12. data/lib/assets/javascripts/firehose/lib/web_socket.js +389 -0
  13. data/lib/assets/javascripts/firehose/long_poll.js.coffee +42 -39
  14. data/lib/assets/javascripts/firehose/transport.js.coffee +1 -1
  15. data/lib/assets/javascripts/firehose/web_socket.js.coffee +8 -14
  16. data/lib/firehose.rb +12 -17
  17. data/lib/firehose/channel.rb +84 -0
  18. data/lib/firehose/cli.rb +57 -13
  19. data/lib/firehose/client.rb +92 -0
  20. data/lib/firehose/default.rb +2 -2
  21. data/lib/firehose/logging.rb +35 -0
  22. data/lib/firehose/producer.rb +1 -0
  23. data/lib/firehose/publisher.rb +56 -4
  24. data/lib/firehose/rack.rb +37 -120
  25. data/lib/firehose/rack/consumer_app.rb +143 -0
  26. data/lib/firehose/rack/ping_app.rb +84 -0
  27. data/lib/firehose/rack/publisher_app.rb +40 -0
  28. data/lib/firehose/server.rb +48 -0
  29. data/lib/firehose/subscriber.rb +54 -0
  30. data/lib/firehose/swf_policy_request.rb +23 -0
  31. data/lib/firehose/version.rb +2 -2
  32. data/lib/rainbows_em_swf_policy.rb +33 -0
  33. data/lib/thin_em_swf_policy.rb +26 -0
  34. data/spec/integrations/integration_test_helper.rb +16 -0
  35. data/spec/integrations/rainbows_spec.rb +7 -0
  36. data/spec/integrations/shared_examples.rb +111 -0
  37. data/spec/integrations/thin_spec.rb +5 -79
  38. data/spec/lib/channel_spec.rb +164 -0
  39. data/spec/lib/client_spec.rb +9 -0
  40. data/spec/lib/default_spec.rb +2 -2
  41. data/spec/lib/publisher_spec.rb +82 -0
  42. data/spec/lib/rack/consumer_app_spec.rb +11 -0
  43. data/spec/lib/rack/ping_app_spec.rb +28 -0
  44. data/spec/lib/rack/publisher_app_spec.rb +26 -0
  45. data/spec/lib/subscriber_spec.rb +69 -0
  46. data/spec/spec_helper.rb +49 -8
  47. metadata +114 -45
  48. data/config.ru +0 -6
  49. data/lib/firehose/subscription.rb +0 -77
data/.env.sample ADDED
@@ -0,0 +1,10 @@
1
+ # This is picked up by em-hiredis
2
+ REDIS_URL=redis://127.0.0.1:6379/0
3
+ RACK_ENV=development
4
+ LOG_LEVEL=debug
5
+ # Which port to bind to
6
+ PORT=7474
7
+ # Which IP address to bind to
8
+ HOST=127.0.0.1
9
+ # Can be rainbows or thin
10
+ SERVER=rainbows
data/.gitignore CHANGED
@@ -1,6 +1,8 @@
1
1
  *.gem
2
2
  .bundle
3
3
  .DS_Store
4
+ *.swp
5
+ .env
4
6
  Gemfile.lock
5
7
  log/*
6
8
  pkg/*
data/Procfile CHANGED
@@ -1 +1 @@
1
- firehose: firehose server
1
+ firehose: ./bin/firehose server -p $PORT
data/README.md CHANGED
@@ -5,20 +5,16 @@
5
5
  | | | | | | __/ | | | (_) \__ \ __/
6
6
  |_| |_|_| \___|_| |_|\___/|___/\___|
7
7
 
8
- Build Realtime web applications in Ruby
8
+ Build realtime web applications in Ruby and JS
9
9
 
10
10
  # What is Firehose?
11
11
 
12
- Firehose is both a Rack application and JavasScript library that makes building scalable real-time web applications possible.
12
+ Firehose is both a Rack application and JavaScript library that makes building real-time web applications possible.
13
13
 
14
14
  # Getting Started
15
15
 
16
- First, you'll need to install and run Redis.
17
-
18
- ```sh
19
- $ apt-get install redis # Install on Ubuntu
20
- $ brew install redis # Install on Mac Homebrew
21
- ```
16
+ First, you'll need to [install and run Redis 2.6](http://redis.io/download).
17
+ Version 2.6 is required because Firehose uses [Lua/EVAL](http://redis.io/commands/eval) for its transactions, which is not available in earlier versions of Redis.
22
18
 
23
19
  Then install the gem.
24
20
 
@@ -30,14 +26,14 @@ $ gem install firehose
30
26
 
31
27
  Now fire up the server.
32
28
 
33
- ```ruby
29
+ ```
34
30
  $ firehose server
35
31
  >> Thin web server (v1.3.1 codename Triple Espresso)
36
32
  >> Maximum connections set to 1024
37
33
  >> Listening on 127.0.0.1:7474, CTRL+C to stop
38
34
  ```
39
35
 
40
- In case you're wondering, the Firehose application server runs the Rack app `Firehose::Rack::App.new` inside of Thin.
36
+ In case you're wondering, the Firehose application server runs the Rack app `Firehose::Rack::App.new` inside of Thin or Rainbows!.
41
37
 
42
38
  ## Publish a message to a bunch of subscribers
43
39
 
@@ -72,6 +68,9 @@ Firehose doesn't just stop at curl; it has a full-featured JavaScript client tha
72
68
  Still have the server running? Copy and paste the code below into Firebug or the WebKit console.
73
69
 
74
70
  ```javascript
71
+ // You'll need to set for WS to work in IE. The value here is the default.
72
+ window.WEB_SOCKET_SWF_LOCATION = '/assets/firehose/WebSocketMain.swf';
73
+
75
74
  new Firehose.Consumer({
76
75
  message: function(msg){
77
76
  console.log(msg);
@@ -103,4 +102,111 @@ socket.io attempts to store connection state per node instance. Firehose makes n
103
102
 
104
103
  Also, socket.io attempts to abstract a low-latency full-duplex port. Firehose assumes that its impossible to simulate this in older web browsers that don't support WebSockets. As such, Firehose focuses on low-latency server-to-client connections and encourages the use of existing HTTP transports, like POST and PUT, for client-to-server communications.
105
104
 
106
- Finally, Firehose attempts to solve data consistency issues and authentication by encourage the use of proxying to the web application.
105
+ Finally, Firehose attempts to solve data consistency issues and authentication by encouraging the use of proxying to the web application.
106
+
107
+ # The Ruby Publisher
108
+
109
+ While you can certainly make your own PUT requests when publishing messages, Firehose includes a Ruby client for easy publishing.
110
+
111
+ ```ruby
112
+ require 'firehose'
113
+ require 'json'
114
+ json = {'hello'=> 'world'}.to_json
115
+ firehose = Firehose::Producer.new('//127.0.0.1:7474')
116
+ firehose.publish(json).to("/my/messages/path")
117
+ ```
118
+
119
+ # Configuration
120
+
121
+ Most configuration happens inside the `.env` file. Take a look at `.env.sample` for more info.
122
+
123
+ ## Sprockets
124
+
125
+ Using Sprockets is the recommended method of including the included client-side assets in a web page.
126
+
127
+ 1) Add the firehose gem in your app's Gemfile.
128
+
129
+ 2) Append the firehose gem's assets to the sprockets path. In a Rails app, this is usually done in an initializer.
130
+
131
+ ```ruby
132
+ gem_path = Gem.loaded_specs['firehose'].full_gem_path
133
+ sprockets.append_path File.join(gem_path, 'lib/assets/flash')
134
+ sprockets.append_path File.join(gem_path, 'lib/assets/javascripts')
135
+ ```
136
+
137
+ NOTE: Some spockets setups will do this automatically. If that is your case, then you can skip this step.
138
+
139
+ 3) Create a firehose config file for setting constants. A good place for this file in a Rails app is `app/assets/javascripts/lib/firehose_config.js.erb`. This gives you a way to configure Flash Web Sockets needed to make some older browser such as IE<10 and Opera<12 support web sockets. Usually this config file will just be:
140
+
141
+ ```erb
142
+ window.WEB_SOCKET_SWF_LOCATION = '<%= asset_path('firehose/WebSocketMain.swf') %>'
143
+ ```
144
+
145
+ The options you can set in this file come directly from https://github.com/gimite/web-socket-js (which is the flash web socket implementation used by Firehose).
146
+
147
+ 4) Require your config file and the firehose gem. This would look something like this:
148
+
149
+ ```ruby
150
+ #= require some/other/js/file
151
+ #= require lib/firehose_config
152
+ #= require firehose
153
+ #= require some/more/js/files
154
+ ```
155
+
156
+ It is important that your config file comes first.
157
+
158
+
159
+ # Web Server
160
+
161
+ Firehose currently supports Thin and Rainbows! (which is the default). Neither is listed as a dependency in the gemspec so that you don't need to install whichever one you aren't using. You can set which server to use via the `.env` file (recommended) or with the `-s` option to `bin/firehose`.
162
+
163
+ # Exception Notification
164
+
165
+ If you'd like to be notified of exceptions, add something like this in your custom config.ru file.
166
+
167
+ ```ruby
168
+ # Use exceptional to handle anything missed by Rack::Exceptional
169
+ if exceptional_key = ENV['EXCEPTIONAL_KEY']
170
+ require 'exceptional'
171
+ EM.error_handler do |e|
172
+ Firehose.logger.error "Unhandled exception: #{e.class} #{e.message}\n#{e.backtrace.join "\n"}"
173
+ ::Exceptional.handle(e)
174
+ end
175
+ end
176
+ ```
177
+
178
+ # Flash Policy File
179
+
180
+ This works out of the box in development if you use the included Procfile with Foreman. However, that solution doesn't work well with a lot production setups, so it is disabled in other environments.
181
+
182
+ [Here is a hack to server your policy file via Nginx](http://blog.vokle.com/index.php/2009/06/10/dealing-with-adobe-and-serving-socket-policy-servers-via-nginx-and-10-lines-of-code/)
183
+
184
+ You can also add a similar hack to HAProxy to serve your policy file.
185
+
186
+ ```sh
187
+ listen swf_policy_requests 0.0.0.0:843
188
+ # Be careful here. This needs to be an absolute path.
189
+ errorfile 400 /path/to/my/crossdomain.xml
190
+ ```
191
+
192
+ In either case, you'll want to be careful about using these hacks on ports that you are also using for genuine HTTP traffic.
193
+
194
+ # Deployment
195
+
196
+ The recommended method of deploying firehose is to deploy it separately from your main app.
197
+
198
+ 1) Create a new project with a Gemfile such as
199
+
200
+ ```
201
+ gem "firehose"
202
+ gem "airbrake"
203
+ gem "rainbows", :require => false
204
+ gem "foreman", :require => false
205
+ gem "capistrano", :require => false
206
+ ```
207
+
208
+ Of course, you could use `exceptional` instead of `airbrake` and `thin` instead of `rainbows`.
209
+
210
+ 2) Set up `config/deploy.rb` to your liking. You can follow most directions for using Capistrano and Foreman to deploy Rack apps, such as https://gist.github.com/1027117
211
+
212
+ 3) Set up `config/rainbows.rb` (if you are using Rainbows!). The gem includes an example to get you started.
@@ -0,0 +1,20 @@
1
+ # TODO - Dunno what a lot of this stuff is... Tune with benchmarks
2
+ # http://rainbows.rubyforge.org/Rainbows/Configurator.html
3
+
4
+ Rainbows! do
5
+ use :EventMachine # concurrency model
6
+ worker_connections 400
7
+ keepalive_timeout 0 # disables keepalives
8
+ keepalive_requests 666 # default:100
9
+ client_max_body_size 5 * 1024 * 1024 # 5 megabytes
10
+ client_header_buffer_size 2 * 1024 # 2 kilobytes
11
+ end
12
+
13
+ # the rest of the Unicorn configuration...
14
+ worker_processes [ENV['WORKER_PROCESSES'].to_i, 1].max # Default to 1
15
+ working_directory ENV['WORKING_DIRECTORY'] if ENV['WORKING_DIRECTORY']
16
+ logger Firehose.logger
17
+
18
+ after_fork do |server, worker|
19
+ require 'rainbows_em_swf_policy'
20
+ end if ENV['RACK_ENV'] == 'development'
data/firehose.gemspec CHANGED
@@ -5,8 +5,8 @@ require "firehose/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "firehose"
7
7
  s.version = Firehose::VERSION
8
- s.authors = ["Brad Gessler", "Steel Fu"]
9
- s.email = ["brad@polleverywhere.com", "steel@polleverywhere.com"]
8
+ s.authors = ["Brad Gessler", "Steel Fu", "Paul Cortens"]
9
+ s.email = ["brad@polleverywhere.com", "steel@polleverywhere.com", "paul@polleverywhere.com"]
10
10
  s.homepage = "http://firehose.io/"
11
11
  s.summary = %q{Build realtime Ruby web applications}
12
12
  s.description = %q{Firehose is a realtime web application toolkit for building realtime Ruby web applications.}
@@ -19,12 +19,11 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  # specify any dependencies here; for example:
22
- s.add_runtime_dependency "eventmachine", ">= 1.0.0.beta"
22
+ s.add_runtime_dependency "eventmachine", ">= 1.0.0.rc"
23
23
  s.add_runtime_dependency "em-hiredis"
24
- s.add_runtime_dependency "thin"
25
24
  s.add_runtime_dependency "thor"
26
25
  s.add_runtime_dependency "faraday"
27
- s.add_runtime_dependency "websocket-rack"
26
+ s.add_runtime_dependency "faye-websocket"
28
27
  s.add_runtime_dependency "em-http-request", "~> 1.0.0"
29
28
 
30
29
  s.add_development_dependency "rspec"
@@ -32,5 +31,9 @@ Gem::Specification.new do |s|
32
31
  s.add_development_dependency "guard-rspec"
33
32
  s.add_development_dependency "guard-bundler"
34
33
  s.add_development_dependency "guard-coffeescript"
35
- s.add_development_dependency "em-websocket-client"
34
+ s.add_development_dependency "rainbows"
35
+ s.add_development_dependency "thin"
36
+ s.add_development_dependency "rack-test"
37
+ s.add_development_dependency "async_rack_test"
38
+ s.add_development_dependency "foreman"
36
39
  end
@@ -1,5 +1,8 @@
1
1
  #= require firehose/base
2
2
  #= require firehose/transport
3
+ #= require firehose/lib/jquery.cors.headers
3
4
  #= require firehose/long_poll
4
5
  #= require firehose/web_socket
5
- #= require firehose/consumer
6
+ #= require firehose/consumer
7
+ #= require firehose/lib/swfobject
8
+ #= require firehose/lib/web_socket
@@ -2,15 +2,7 @@ class Firehose.Consumer
2
2
  # Transports that are available to Firehose.
3
3
  @transports: [Firehose.WebSocket, Firehose.LongPoll]
4
4
 
5
- # Generate a random consumer id.
6
- @nextConsumerId: ->
7
- Math.floor((Math.random()*99999999999)+1)
8
-
9
5
  constructor: (config = {}) ->
10
- # The consumerId is used by the server to remember messages between requests. In a production environment,
11
- # this should probably be some combination of "user_id-rand". Why the rand? Because a user may have multiple
12
- # tabs open to the application, and each tab needs a different channel on the server.
13
- config.consumerId ||= Firehose.Consumer.nextConsumerId()
14
6
  # List of transport stragies we have to use.
15
7
  config.transports ||= Firehose.Consumer.transports
16
8
  # Empty handler for messages.
@@ -27,8 +19,8 @@ class Firehose.Consumer
27
19
  throw "Could not connect"
28
20
  # URL that we'll connect to.
29
21
  config.uri ||= undefined
30
- # Params that we'll tack on to the URL. We include a consumerId in here for kicks.
31
- config.params ||= { cid: config.consumerId }
22
+ # Params that we'll tack on to the URL.
23
+ config.params ||= { }
32
24
  # Do stuff before we send the message into config.message. The sensible
33
25
  # default on the webs is to parse JSON.
34
26
  config.parse ||= (body) ->
@@ -58,4 +50,4 @@ class Firehose.Consumer
58
50
  @config.failed() # Call the original fail method passed into the Firehose.Consumer
59
51
  new transport(config)
60
52
  # Fire off the first connection attempt.
61
- transports[0].connect()
53
+ transports[0].connect()
@@ -0,0 +1,16 @@
1
+ _super = jQuery.ajaxSettings.xhr
2
+
3
+ jQuery.ajaxSettings.xhr = ->
4
+ xhr = _super()
5
+ getAllResponseHeaders = xhr.getAllResponseHeaders
6
+ xhr.getAllResponseHeaders = ->
7
+ allHeaders = getAllResponseHeaders.call(xhr)
8
+ return allHeaders if allHeaders
9
+
10
+ allHeaders = ""
11
+ for headerName in [ "Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma" ]
12
+ do (headerName) ->
13
+ allHeaders += headerName + ": " + xhr.getResponseHeader(headerName) + "\n" if xhr.getResponseHeader(headerName)
14
+
15
+ allHeaders
16
+ xhr
@@ -0,0 +1,4 @@
1
+ /* SWFObject v2.2 <http://code.google.com/p/swfobject/>
2
+ is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
3
+ */
4
+ var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y<X;Y++){U[Y]()}}function K(X){if(J){X()}else{U[U.length]=X}}function s(Y){if(typeof O.addEventListener!=D){O.addEventListener("load",Y,false)}else{if(typeof j.addEventListener!=D){j.addEventListener("load",Y,false)}else{if(typeof O.attachEvent!=D){i(O,"onload",Y)}else{if(typeof O.onload=="function"){var X=O.onload;O.onload=function(){X();Y()}}else{O.onload=Y}}}}}function h(){if(T){V()}else{H()}}function V(){var X=j.getElementsByTagName("body")[0];var aa=C(r);aa.setAttribute("type",q);var Z=X.appendChild(aa);if(Z){var Y=0;(function(){if(typeof Z.GetVariable!=D){var ab=Z.GetVariable("$version");if(ab){ab=ab.split(" ")[1].split(",");M.pv=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}else{if(Y<10){Y++;setTimeout(arguments.callee,10);return}}X.removeChild(aa);Z=null;H()})()}else{H()}}function H(){var ag=o.length;if(ag>0){for(var af=0;af<ag;af++){var Y=o[af].id;var ab=o[af].callbackFn;var aa={success:false,id:Y};if(M.pv[0]>0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad<ac;ad++){if(X[ad].getAttribute("name").toLowerCase()!="movie"){ah[X[ad].getAttribute("name")]=X[ad].getAttribute("value")}}P(ai,ah,Y,ab)}else{p(ae);if(ab){ab(aa)}}}}}else{w(Y,true);if(ab){var Z=z(Y);if(Z&&typeof Z.SetVariable!=D){aa.success=true;aa.ref=Z}ab(aa)}}}}}function z(aa){var X=null;var Y=c(aa);if(Y&&Y.nodeName=="OBJECT"){if(typeof Y.SetVariable!=D){X=Y}else{var Z=Y.getElementsByTagName(r)[0];if(Z){X=Z}}}return X}function A(){return !a&&F("6.0.65")&&(M.win||M.mac)&&!(M.wk&&M.wk<312)}function P(aa,ab,X,Z){a=true;E=Z||null;B={success:false,id:X};var ae=c(X);if(ae){if(ae.nodeName=="OBJECT"){l=g(ae);Q=null}else{l=ae;Q=X}aa.id=R;if(typeof aa.width==D||(!/%$/.test(aa.width)&&parseInt(aa.width,10)<310)){aa.width="310"}if(typeof aa.height==D||(!/%$/.test(aa.height)&&parseInt(aa.height,10)<137)){aa.height="137"}j.title=j.title.slice(0,47)+" - Flash Player Installation";var ad=M.ie&&M.win?"ActiveX":"PlugIn",ac="MMredirectURL="+O.location.toString().replace(/&/g,"%26")+"&MMplayerType="+ad+"&MMdoctitle="+j.title;if(typeof ab.flashvars!=D){ab.flashvars+="&"+ac}else{ab.flashvars=ac}if(M.ie&&M.win&&ae.readyState!=4){var Y=C("div");X+="SWFObjectNew";Y.setAttribute("id",X);ae.parentNode.insertBefore(Y,ae);ae.style.display="none";(function(){if(ae.readyState==4){ae.parentNode.removeChild(ae)}else{setTimeout(arguments.callee,10)}})()}u(aa,ab,X)}}function p(Y){if(M.ie&&M.win&&Y.readyState!=4){var X=C("div");Y.parentNode.insertBefore(X,Y);X.parentNode.replaceChild(g(Y),X);Y.style.display="none";(function(){if(Y.readyState==4){Y.parentNode.removeChild(Y)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.replaceChild(g(Y),Y)}}function g(ab){var aa=C("div");if(M.win&&M.ie){aa.innerHTML=ab.innerHTML}else{var Y=ab.getElementsByTagName(r)[0];if(Y){var ad=Y.childNodes;if(ad){var X=ad.length;for(var Z=0;Z<X;Z++){if(!(ad[Z].nodeType==1&&ad[Z].nodeName=="PARAM")&&!(ad[Z].nodeType==8)){aa.appendChild(ad[Z].cloneNode(true))}}}}}return aa}function u(ai,ag,Y){var X,aa=c(Y);if(M.wk&&M.wk<312){return X}if(aa){if(typeof ai.id==D){ai.id=Y}if(M.ie&&M.win){var ah="";for(var ae in ai){if(ai[ae]!=Object.prototype[ae]){if(ae.toLowerCase()=="data"){ag.movie=ai[ae]}else{if(ae.toLowerCase()=="styleclass"){ah+=' class="'+ai[ae]+'"'}else{if(ae.toLowerCase()!="classid"){ah+=" "+ae+'="'+ai[ae]+'"'}}}}}var af="";for(var ad in ag){if(ag[ad]!=Object.prototype[ad]){af+='<param name="'+ad+'" value="'+ag[ad]+'" />'}}aa.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+ah+">"+af+"</object>";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab<ac;ab++){I[ab][0].detachEvent(I[ab][1],I[ab][2])}var Z=N.length;for(var aa=0;aa<Z;aa++){y(N[aa])}for(var Y in M){M[Y]=null}M=null;for(var X in swfobject){swfobject[X]=null}swfobject=null})}}();return{registerObject:function(ab,X,aa,Z){if(M.w3&&ab&&X){var Y={};Y.id=ab;Y.swfVersion=X;Y.expressInstall=aa;Y.callbackFn=Z;o[o.length]=Y;w(ab,false)}else{if(Z){Z({success:false,id:ab})}}},getObjectById:function(X){if(M.w3){return z(X)}},embedSWF:function(ab,ah,ae,ag,Y,aa,Z,ad,af,ac){var X={success:false,id:ah};if(M.w3&&!(M.wk&&M.wk<312)&&ab&&ah&&ae&&ag&&Y){w(ah,false);K(function(){ae+="";ag+="";var aj={};if(af&&typeof af===r){for(var al in af){aj[al]=af[al]}}aj.data=ab;aj.width=ae;aj.height=ag;var am={};if(ad&&typeof ad===r){for(var ak in ad){am[ak]=ad[ak]}}if(Z&&typeof Z===r){for(var ai in Z){if(typeof am.flashvars!=D){am.flashvars+="&"+ai+"="+Z[ai]}else{am.flashvars=ai+"="+Z[ai]}}}if(F(Y)){var an=u(aj,am,ah);if(aj.id==ah){w(ah,true)}X.success=true;X.ref=an}else{if(aa&&A()){aj.data=aa;P(aj,am,ah,ac);return}else{w(ah,true)}}if(ac){ac(X)}})}else{if(ac){ac(X)}}},switchOffAutoHideShow:function(){m=false},ua:M,getFlashPlayerVersion:function(){return{major:M.pv[0],minor:M.pv[1],release:M.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(Z,Y,X){if(M.w3){return u(Z,Y,X)}else{return undefined}},showExpressInstall:function(Z,aa,X,Y){if(M.w3&&A()){P(Z,aa,X,Y)}},removeSWF:function(X){if(M.w3){y(X)}},createCSS:function(aa,Z,Y,X){if(M.w3){v(aa,Z,Y,X)}},addDomLoadEvent:K,addLoadEvent:s,getQueryParamValue:function(aa){var Z=j.location.search||j.location.hash;if(Z){if(/\?/.test(Z)){Z=Z.split("?")[1]}if(aa==null){return L(Z)}var Y=Z.split("&");for(var X=0;X<Y.length;X++){if(Y[X].substring(0,Y[X].indexOf("="))==aa){return L(Y[X].substring((Y[X].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(a){var X=c(R);if(X&&l){X.parentNode.replaceChild(l,X);if(Q){w(Q,true);if(M.ie&&M.win){l.style.display="block"}}if(E){E(B)}}a=false}}}}();
@@ -0,0 +1,389 @@
1
+ // Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ // License: New BSD License
3
+ // Reference: http://dev.w3.org/html5/websockets/
4
+ // Reference: http://tools.ietf.org/html/rfc6455
5
+
6
+ (function() {
7
+
8
+ if (window.WEB_SOCKET_FORCE_FLASH) {
9
+ // Keeps going.
10
+ } else if (window.WebSocket) {
11
+ return;
12
+ } else if (window.MozWebSocket) {
13
+ // Firefox.
14
+ window.WebSocket = MozWebSocket;
15
+ return;
16
+ }
17
+
18
+ var logger;
19
+ if (window.WEB_SOCKET_LOGGER) {
20
+ logger = WEB_SOCKET_LOGGER;
21
+ } else if (window.console && window.console.log && window.console.error) {
22
+ // In some environment, console is defined but console.log or console.error is missing.
23
+ logger = window.console;
24
+ } else {
25
+ logger = {log: function(){ }, error: function(){ }};
26
+ }
27
+
28
+ // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
29
+ if (swfobject.getFlashPlayerVersion().major < 10) {
30
+ logger.error("Flash Player >= 10.0.0 is required.");
31
+ return;
32
+ }
33
+ if (location.protocol == "file:") {
34
+ logger.error(
35
+ "WARNING: web-socket-js doesn't work in file:///... URL " +
36
+ "unless you set Flash Security Settings properly. " +
37
+ "Open the page via Web server i.e. http://...");
38
+ }
39
+
40
+ /**
41
+ * Our own implementation of WebSocket class using Flash.
42
+ * @param {string} url
43
+ * @param {array or string} protocols
44
+ * @param {string} proxyHost
45
+ * @param {int} proxyPort
46
+ * @param {string} headers
47
+ */
48
+ window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
49
+ var self = this;
50
+ self.__id = WebSocket.__nextId++;
51
+ WebSocket.__instances[self.__id] = self;
52
+ self.readyState = WebSocket.CONNECTING;
53
+ self.bufferedAmount = 0;
54
+ self.__events = {};
55
+ if (!protocols) {
56
+ protocols = [];
57
+ } else if (typeof protocols == "string") {
58
+ protocols = [protocols];
59
+ }
60
+ // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
61
+ // Otherwise, when onopen fires immediately, onopen is called before it is set.
62
+ self.__createTask = setTimeout(function() {
63
+ WebSocket.__addTask(function() {
64
+ self.__createTask = null;
65
+ WebSocket.__flash.create(
66
+ self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
67
+ });
68
+ }, 0);
69
+ };
70
+
71
+ /**
72
+ * Send data to the web socket.
73
+ * @param {string} data The data to send to the socket.
74
+ * @return {boolean} True for success, false for failure.
75
+ */
76
+ WebSocket.prototype.send = function(data) {
77
+ if (this.readyState == WebSocket.CONNECTING) {
78
+ throw "INVALID_STATE_ERR: Web Socket connection has not been established";
79
+ }
80
+ // We use encodeURIComponent() here, because FABridge doesn't work if
81
+ // the argument includes some characters. We don't use escape() here
82
+ // because of this:
83
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
84
+ // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
85
+ // preserve all Unicode characters either e.g. "\uffff" in Firefox.
86
+ // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require
87
+ // additional testing.
88
+ var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
89
+ if (result < 0) { // success
90
+ return true;
91
+ } else {
92
+ this.bufferedAmount += result;
93
+ return false;
94
+ }
95
+ };
96
+
97
+ /**
98
+ * Close this web socket gracefully.
99
+ */
100
+ WebSocket.prototype.close = function() {
101
+ if (this.__createTask) {
102
+ clearTimeout(this.__createTask);
103
+ this.__createTask = null;
104
+ this.readyState = WebSocket.CLOSED;
105
+ return;
106
+ }
107
+ if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
108
+ return;
109
+ }
110
+ this.readyState = WebSocket.CLOSING;
111
+ WebSocket.__flash.close(this.__id);
112
+ };
113
+
114
+ /**
115
+ * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
116
+ *
117
+ * @param {string} type
118
+ * @param {function} listener
119
+ * @param {boolean} useCapture
120
+ * @return void
121
+ */
122
+ WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
123
+ if (!(type in this.__events)) {
124
+ this.__events[type] = [];
125
+ }
126
+ this.__events[type].push(listener);
127
+ };
128
+
129
+ /**
130
+ * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
131
+ *
132
+ * @param {string} type
133
+ * @param {function} listener
134
+ * @param {boolean} useCapture
135
+ * @return void
136
+ */
137
+ WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
138
+ if (!(type in this.__events)) return;
139
+ var events = this.__events[type];
140
+ for (var i = events.length - 1; i >= 0; --i) {
141
+ if (events[i] === listener) {
142
+ events.splice(i, 1);
143
+ break;
144
+ }
145
+ }
146
+ };
147
+
148
+ /**
149
+ * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
150
+ *
151
+ * @param {Event} event
152
+ * @return void
153
+ */
154
+ WebSocket.prototype.dispatchEvent = function(event) {
155
+ var events = this.__events[event.type] || [];
156
+ for (var i = 0; i < events.length; ++i) {
157
+ events[i](event);
158
+ }
159
+ var handler = this["on" + event.type];
160
+ if (handler) handler.apply(this, [event]);
161
+ };
162
+
163
+ /**
164
+ * Handles an event from Flash.
165
+ * @param {Object} flashEvent
166
+ */
167
+ WebSocket.prototype.__handleEvent = function(flashEvent) {
168
+
169
+ if ("readyState" in flashEvent) {
170
+ this.readyState = flashEvent.readyState;
171
+ }
172
+ if ("protocol" in flashEvent) {
173
+ this.protocol = flashEvent.protocol;
174
+ }
175
+
176
+ var jsEvent;
177
+ if (flashEvent.type == "open" || flashEvent.type == "error") {
178
+ jsEvent = this.__createSimpleEvent(flashEvent.type);
179
+ } else if (flashEvent.type == "close") {
180
+ jsEvent = this.__createSimpleEvent("close");
181
+ jsEvent.wasClean = flashEvent.wasClean ? true : false;
182
+ jsEvent.code = flashEvent.code;
183
+ jsEvent.reason = flashEvent.reason;
184
+ } else if (flashEvent.type == "message") {
185
+ var data = decodeURIComponent(flashEvent.message);
186
+ jsEvent = this.__createMessageEvent("message", data);
187
+ } else {
188
+ throw "unknown event type: " + flashEvent.type;
189
+ }
190
+
191
+ this.dispatchEvent(jsEvent);
192
+
193
+ };
194
+
195
+ WebSocket.prototype.__createSimpleEvent = function(type) {
196
+ if (document.createEvent && window.Event) {
197
+ var event = document.createEvent("Event");
198
+ event.initEvent(type, false, false);
199
+ return event;
200
+ } else {
201
+ return {type: type, bubbles: false, cancelable: false};
202
+ }
203
+ };
204
+
205
+ WebSocket.prototype.__createMessageEvent = function(type, data) {
206
+ if (document.createEvent && window.MessageEvent && !window.opera) {
207
+ var event = document.createEvent("MessageEvent");
208
+ event.initMessageEvent("message", false, false, data, null, null, window, null);
209
+ return event;
210
+ } else {
211
+ // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
212
+ return {type: type, data: data, bubbles: false, cancelable: false};
213
+ }
214
+ };
215
+
216
+ /**
217
+ * Define the WebSocket readyState enumeration.
218
+ */
219
+ WebSocket.CONNECTING = 0;
220
+ WebSocket.OPEN = 1;
221
+ WebSocket.CLOSING = 2;
222
+ WebSocket.CLOSED = 3;
223
+
224
+ WebSocket.__initialized = false;
225
+ WebSocket.__flash = null;
226
+ WebSocket.__instances = {};
227
+ WebSocket.__tasks = [];
228
+ WebSocket.__nextId = 0;
229
+
230
+ /**
231
+ * Load a new flash security policy file.
232
+ * @param {string} url
233
+ */
234
+ WebSocket.loadFlashPolicyFile = function(url){
235
+ WebSocket.__addTask(function() {
236
+ WebSocket.__flash.loadManualPolicyFile(url);
237
+ });
238
+ };
239
+
240
+ /**
241
+ * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
242
+ */
243
+ WebSocket.__initialize = function() {
244
+
245
+ if (WebSocket.__initialized) return;
246
+ WebSocket.__initialized = true;
247
+
248
+ if (WebSocket.__swfLocation) {
249
+ // For backword compatibility.
250
+ window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
251
+ }
252
+ if (!window.WEB_SOCKET_SWF_LOCATION) {
253
+ logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
254
+ return;
255
+ }
256
+ if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR &&
257
+ !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
258
+ WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
259
+ var swfHost = RegExp.$1;
260
+ if (location.host != swfHost) {
261
+ logger.error(
262
+ "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
263
+ "('" + location.host + "' != '" + swfHost + "'). " +
264
+ "See also 'How to host HTML file and SWF file in different domains' section " +
265
+ "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " +
266
+ "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;");
267
+ }
268
+ }
269
+ var container = document.createElement("div");
270
+ container.id = "webSocketContainer";
271
+ // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
272
+ // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
273
+ // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
274
+ // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
275
+ // the best we can do as far as we know now.
276
+ container.style.position = "absolute";
277
+ if (WebSocket.__isFlashLite()) {
278
+ container.style.left = "0px";
279
+ container.style.top = "0px";
280
+ } else {
281
+ container.style.left = "-100px";
282
+ container.style.top = "-100px";
283
+ }
284
+ var holder = document.createElement("div");
285
+ holder.id = "webSocketFlash";
286
+ container.appendChild(holder);
287
+ document.body.appendChild(container);
288
+ // See this article for hasPriority:
289
+ // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
290
+ swfobject.embedSWF(
291
+ WEB_SOCKET_SWF_LOCATION,
292
+ "webSocketFlash",
293
+ "1" /* width */,
294
+ "1" /* height */,
295
+ "10.0.0" /* SWF version */,
296
+ null,
297
+ null,
298
+ {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
299
+ null,
300
+ function(e) {
301
+ if (!e.success) {
302
+ logger.error("[WebSocket] swfobject.embedSWF failed");
303
+ }
304
+ }
305
+ );
306
+
307
+ };
308
+
309
+ /**
310
+ * Called by Flash to notify JS that it's fully loaded and ready
311
+ * for communication.
312
+ */
313
+ WebSocket.__onFlashInitialized = function() {
314
+ // We need to set a timeout here to avoid round-trip calls
315
+ // to flash during the initialization process.
316
+ setTimeout(function() {
317
+ WebSocket.__flash = document.getElementById("webSocketFlash");
318
+ WebSocket.__flash.setCallerUrl(location.href);
319
+ WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
320
+ for (var i = 0; i < WebSocket.__tasks.length; ++i) {
321
+ WebSocket.__tasks[i]();
322
+ }
323
+ WebSocket.__tasks = [];
324
+ }, 0);
325
+ };
326
+
327
+ /**
328
+ * Called by Flash to notify WebSockets events are fired.
329
+ */
330
+ WebSocket.__onFlashEvent = function() {
331
+ setTimeout(function() {
332
+ try {
333
+ // Gets events using receiveEvents() instead of getting it from event object
334
+ // of Flash event. This is to make sure to keep message order.
335
+ // It seems sometimes Flash events don't arrive in the same order as they are sent.
336
+ var events = WebSocket.__flash.receiveEvents();
337
+ for (var i = 0; i < events.length; ++i) {
338
+ WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
339
+ }
340
+ } catch (e) {
341
+ logger.error(e);
342
+ }
343
+ }, 0);
344
+ return true;
345
+ };
346
+
347
+ // Called by Flash.
348
+ WebSocket.__log = function(message) {
349
+ logger.log(decodeURIComponent(message));
350
+ };
351
+
352
+ // Called by Flash.
353
+ WebSocket.__error = function(message) {
354
+ logger.error(decodeURIComponent(message));
355
+ };
356
+
357
+ WebSocket.__addTask = function(task) {
358
+ if (WebSocket.__flash) {
359
+ task();
360
+ } else {
361
+ WebSocket.__tasks.push(task);
362
+ }
363
+ };
364
+
365
+ /**
366
+ * Test if the browser is running flash lite.
367
+ * @return {boolean} True if flash lite is running, false otherwise.
368
+ */
369
+ WebSocket.__isFlashLite = function() {
370
+ if (!window.navigator || !window.navigator.mimeTypes) {
371
+ return false;
372
+ }
373
+ var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
374
+ if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
375
+ return false;
376
+ }
377
+ return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
378
+ };
379
+
380
+ if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
381
+ // NOTE:
382
+ // This fires immediately if web_socket.js is dynamically loaded after
383
+ // the document is loaded.
384
+ swfobject.addDomLoadEvent(function() {
385
+ WebSocket.__initialize();
386
+ });
387
+ }
388
+
389
+ })();