sockjs 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENCE +19 -0
  2. data/README.textile +118 -0
  3. data/lib/meta-state.rb +151 -0
  4. data/lib/rack/sockjs.rb +173 -0
  5. data/lib/sockjs.rb +59 -0
  6. data/lib/sockjs/callbacks.rb +19 -0
  7. data/lib/sockjs/connection.rb +45 -0
  8. data/lib/sockjs/delayed-response-body.rb +99 -0
  9. data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
  10. data/lib/sockjs/duck-punch-thin-response.rb +15 -0
  11. data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
  12. data/lib/sockjs/faye.rb +15 -0
  13. data/lib/sockjs/protocol.rb +97 -0
  14. data/lib/sockjs/servers/request.rb +136 -0
  15. data/lib/sockjs/servers/response.rb +169 -0
  16. data/lib/sockjs/session.rb +388 -0
  17. data/lib/sockjs/transport.rb +354 -0
  18. data/lib/sockjs/transports/eventsource.rb +30 -0
  19. data/lib/sockjs/transports/htmlfile.rb +69 -0
  20. data/lib/sockjs/transports/iframe.rb +68 -0
  21. data/lib/sockjs/transports/info.rb +48 -0
  22. data/lib/sockjs/transports/jsonp.rb +84 -0
  23. data/lib/sockjs/transports/websocket.rb +166 -0
  24. data/lib/sockjs/transports/welcome_screen.rb +17 -0
  25. data/lib/sockjs/transports/xhr.rb +75 -0
  26. data/lib/sockjs/version.rb +13 -0
  27. data/spec/sockjs/protocol_spec.rb +49 -0
  28. data/spec/sockjs/session_spec.rb +51 -0
  29. data/spec/sockjs/transport_spec.rb +73 -0
  30. data/spec/sockjs/transports/eventsource_spec.rb +56 -0
  31. data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
  32. data/spec/sockjs/transports/iframe_spec.rb +66 -0
  33. data/spec/sockjs/transports/jsonp_spec.rb +252 -0
  34. data/spec/sockjs/transports/websocket_spec.rb +101 -0
  35. data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
  36. data/spec/sockjs/transports/xhr_spec.rb +314 -0
  37. data/spec/sockjs/version_spec.rb +18 -0
  38. data/spec/sockjs_spec.rb +8 -0
  39. data/spec/spec_helper.rb +121 -0
  40. data/spec/support/async-test.rb +42 -0
  41. metadata +171 -0
data/LICENCE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 VMware, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,118 @@
1
+ h1. About
2
+
3
+ _*Disclaimer:* This library is still work in progress._
4
+
5
+ SockJS is WebSocket emulation library. It means that you use the WebSocket API, only instead of @WebSocket@ class you instantiate @SockJS@ class. I highly recommend to read "SockJS: WebSocket emulation":http://www.rabbitmq.com/blog/2011/09/13/sockjs-websocket-emulation on the RabbitMQ blog for more info.
6
+
7
+ h2. Prerequisites
8
+
9
+ Even though this library uses Rack interface, *Thin is required* as "it supports asynchronous callback":http://macournoyer.com/blog/2009/06/04/pusher-and-async-with-thin. For Websockets, we use "faye-websocket":http://blog.jcoglan.com/2011/11/28/announcing-faye-websocket-a-standards-compliant-websocket-library gem.
10
+
11
+ h2. The Client-Side Part
12
+
13
+ For the client-side part you have to use JS library "sockjs-client":http://sockjs.github.com/sockjs-client which provides WebSocket-like API. Here's an example:
14
+
15
+ <pre>
16
+ <script src="http://cdn.sockjs.org/sockjs-0.2.1.min.js"></script>
17
+
18
+ <script>
19
+ var sock = new SockJS("http://mydomain.com/my_prefix");
20
+
21
+ sock.onopen = function() {
22
+ console.log("open");
23
+ };
24
+
25
+ sock.onmessage = function(e) {
26
+ console.log("message", e.data);
27
+ };
28
+
29
+ sock.onclose = function() {
30
+ console.log("close");
31
+ };
32
+ </script>
33
+ </pre>
34
+
35
+ h2. The Server-Side Part
36
+
37
+ Now in order to have someone to talk to, we need to run a server. That's exactly what is sockjs-ruby good for:
38
+
39
+ <pre>
40
+ #!/usr/bin/env ruby
41
+ # encoding: utf-8
42
+
43
+ require "rack"
44
+ require "rack/sockjs"
45
+ require "eventmachine"
46
+
47
+ # Your custom app.
48
+ class MyHelloWorld
49
+ def call(env)
50
+ body = "This is the app, not SockJS."
51
+ headers = {
52
+ "Content-Type" => "text/plain; charset=UTF-8",
53
+ "Content-Length" => body.bytesize.to_s
54
+ }
55
+
56
+ [200, headers, [body]]
57
+ end
58
+ end
59
+
60
+
61
+ app = Rack::Builder.new do
62
+ # Run one SockJS app on /echo.
63
+ use SockJS, "/echo" do |connection|
64
+ connection.subscribe do |session, message|
65
+ session.send(message)
66
+ end
67
+ end
68
+
69
+ # ... and the other one on /close.
70
+ use SockJS, "/close" do |connection|
71
+ connection.session_open do |session|
72
+ session.close(3000, "Go away!")
73
+ end
74
+ end
75
+
76
+ # This app will run on other URLs than /echo and /close,
77
+ # as these has already been assigned to SockJS.
78
+ run MyHelloWorld.new
79
+ end
80
+
81
+
82
+ EM.run do
83
+ thin = Rack::Handler.get("thin")
84
+ thin.run(app.to_app, Port: 8081)
85
+ end
86
+ </pre>
87
+
88
+ For more complex example check "examples/sockjs_apps_for_sockjs_protocol_tests.rb":https://github.com/sockjs/sockjs-ruby/blob/master/examples/sockjs_apps_for_sockjs_protocol_tests.rb
89
+
90
+
91
+ h2. SockJS Family
92
+
93
+ * "SockJS-client":https://github.com/sockjs/sockjs-client JavaScript client library.
94
+ * "SockJS-node":https://github.com/sockjs/sockjs-node Node.js server.
95
+ * "SockJS-ruby":https://github.com/sockjs/sockjs-ruby Ruby server.
96
+ * "SockJS-protocol":https://github.com/sockjs/sockjs-protocol protocol tests and documentation.
97
+ * "SockJS-protocol spec":http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.2.1.html
98
+
99
+ h1. Development
100
+
101
+ Get "sockjs-protocol":https://github.com/sockjs/sockjs-protocol (installation information are in its README) and run @rake protocol_test@. Now you can run the tests against it, for instance:
102
+
103
+ <pre>
104
+ # Run all the tests.
105
+ ./venv/bin/python sockjs-protocol-0.2.1.py
106
+
107
+ # Run all the tests defined in XhrStreaming.
108
+ ./venv/bin/python sockjs-protocol-0.2.1.py XhrStreaming
109
+
110
+ # Run only XhrStreaming.test_transport test.
111
+ ./venv/bin/python sockjs-protocol-0.2.1.py XhrStreaming.test_transport
112
+ </pre>
113
+
114
+ h1. Links
115
+
116
+ * "SockJS: WebSocket emulation":http://www.rabbitmq.com/blog/2011/09/13/sockjs-websocket-emulation
117
+ * "SockJS: web messaging ain't easy":http://www.rabbitmq.com/blog/2011/08/22/sockjs-web-messaging-aint-easy
118
+ * "PubSubHuddle Realtime Web talk":http://www.rabbitmq.com/blog/2011/09/26/pubsubhuddle-realtime-web-talk
@@ -0,0 +1,151 @@
1
+
2
+
3
+ module MetaState
4
+ class Error < ::StandardError; end
5
+ class WrongStateError < Error; end
6
+ class InvalidStateError < Error; end
7
+
8
+ class Machine
9
+ NON_MESSAGES = [:on_exit, :on_enter]
10
+
11
+ class << self
12
+ def add_state(state)
13
+ @default_state ||= state
14
+ states
15
+ @states[state] = true
16
+ name = state.name.sub(/.*::/,'').downcase
17
+ state_names
18
+ @state_names[name] = state
19
+ @state_names[name.to_sym] = state
20
+ include state
21
+ @void_state_module = nil
22
+ end
23
+
24
+ def state(name, &block)
25
+ mod = Module.new(&block)
26
+ const_set(name, mod)
27
+ add_state(mod)
28
+ end
29
+
30
+ def default_state
31
+ @default_state || superclass.default_state
32
+ end
33
+
34
+ def void_state_module
35
+ if @void_state_module.nil?
36
+ build_void_state
37
+ end
38
+ @void_state_module
39
+ end
40
+
41
+ #Explicitly set the default (i.e. initial state) for an FSM
42
+ #Normally, this defaults to the first state defined, but some folks like
43
+ #to be explicit
44
+ def default_state=(state)
45
+ @default_state = state
46
+ end
47
+
48
+ def state_names
49
+ @state_names ||= {}
50
+ if Machine > superclass
51
+ superclass.state_names.merge(@state_names)
52
+ else
53
+ @state_names
54
+ end
55
+ end
56
+
57
+ def states
58
+ @states ||= {}
59
+ if Machine > superclass
60
+ superclass.states.merge(@states)
61
+ else
62
+ @states
63
+ end
64
+ end
65
+
66
+ def build_void_state
67
+ methods = (self.states.keys.map do |state|
68
+ state.instance_methods
69
+ end.flatten + NON_MESSAGES).uniq
70
+
71
+ @void_state_module = Module.new do
72
+ methods.each do |method|
73
+ if NON_MESSAGES.include?(method)
74
+ define_method(method){}
75
+ else
76
+ define_method(method) do
77
+ raise WrongStateError, "Message #{method} received in state #{current_state}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ include @void_state_module
84
+ end
85
+ end
86
+
87
+ #Explicitly put an FSM into a particular state. Simultaneously enters a
88
+ #state of sin. Use sparingly if at all.
89
+ def state=(state)
90
+ mod = state_module(state)
91
+ assign_state(mod)
92
+ end
93
+
94
+ def debug_with(&block)
95
+ @debug_block = block
96
+ end
97
+
98
+ attr_reader :current_state
99
+ def initialize
100
+ @debug_block = nil
101
+ assign_state(self.class.default_state)
102
+ end
103
+
104
+ protected
105
+
106
+ def debug
107
+ return if @debug_block.nil?
108
+ message = yield
109
+ @debug_block[message]
110
+ end
111
+
112
+ def assign_state(mod)
113
+ force_extend(self.class.void_state_module)
114
+ force_extend(mod)
115
+ @current_state = mod
116
+ end
117
+
118
+ def force_extend(mod)
119
+ mod.instance_methods.each do |method_name|
120
+ define_singleton_method(method_name, mod.instance_method(method_name))
121
+ end
122
+ end
123
+
124
+ def state_module(state)
125
+ unless state.is_a? Module
126
+ state = self.class.state_names[state] unless state.is_a? Module
127
+ end
128
+ raise InvalidStateError unless self.class.states[state]
129
+ return state
130
+ end
131
+
132
+ def transition_to(state)
133
+ target_state = state_module(state)
134
+ return true if target_state == current_state
135
+ source_state = current_state
136
+
137
+ debug{ "Transitioning from #{source_state.inspect} to #{target_state.inspect}" }
138
+
139
+ on_exit
140
+
141
+ warn "State changed after on_exit method. Became: #{current_state.inspect}" unless source_state == current_state
142
+
143
+ assign_state(target_state)
144
+
145
+ on_enter
146
+
147
+ warn "State changed after on_enter method. Became: #{current_state.inspect}" unless target_state == current_state
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,173 @@
1
+ # encoding: utf-8
2
+ require "sockjs"
3
+ require 'sockjs/version'
4
+ require "sockjs/transport"
5
+ require "sockjs/servers/request"
6
+ require "sockjs/servers/response"
7
+
8
+ require 'rack/mount'
9
+
10
+ require 'sockjs/duck-punch-rack-mount'
11
+ require 'sockjs/duck-punch-thin-response'
12
+
13
+ # Transports.
14
+ require "sockjs/transports/info"
15
+ require "sockjs/transports/eventsource"
16
+ require "sockjs/transports/htmlfile"
17
+ require "sockjs/transports/iframe"
18
+ require "sockjs/transports/jsonp"
19
+ require "sockjs/transports/websocket"
20
+ require "sockjs/transports/welcome_screen"
21
+ require "sockjs/transports/xhr"
22
+
23
+ # This is a Rack middleware for SockJS.
24
+ #
25
+ #@example
26
+ #
27
+ # require 'rack/sockjs'
28
+ #
29
+ # map "/echo", Rack::SockJS.new do |connection|
30
+ # connection.subscribe do |session, message|
31
+ # session.send(message)
32
+ # end
33
+ # end
34
+ #
35
+ # run MyApp
36
+ #
37
+ #
38
+ # #or
39
+ #
40
+ # run Rack::SockJS.new do |connection|
41
+ # connection.session_open do |session|
42
+ # session.close(3000, "Go away!")
43
+ # end
44
+ # end
45
+
46
+ module Rack
47
+ class SockJS
48
+ SERVER_SESSION_REGEXP = %r{/([^/]*)/([^/]*)}
49
+ DEFAULT_OPTIONS = {
50
+ :sockjs_url => "http://cdn.sockjs.org/sockjs-#{::SockJS::PROTOCOL_VERSION}.min.js"
51
+ }
52
+
53
+
54
+ class DebugRequest
55
+ def initialize(app)
56
+ @app = app
57
+ end
58
+
59
+ def call(env)
60
+ request = ::SockJS::Request.new(env)
61
+ headers = request.headers.select { |key, value| not %w{version host accept-encoding}.include?(key.to_s) }
62
+ ::SockJS.puts "\n~ \e[31m#{request.http_method} \e[32m#{request.path_info.inspect}#{" " + headers.inspect unless headers.empty?} \e[0m(\e[34m#{@prefix} app\e[0m)"
63
+ headers = headers.map { |key, value| "-H '#{key}: #{value}'" }.join(" ")
64
+ ::SockJS.puts "\e[90mcurl -X #{request.http_method} http://localhost:8081#{request.path_info} #{headers}\e[0m"
65
+
66
+ result = @app.call(env)
67
+ ensure
68
+ ::SockJS.debug "Rack response: " + result.inspect
69
+ end
70
+ end
71
+
72
+ class MissingHandler
73
+ def initialize(options)
74
+ end
75
+
76
+ def call(env)
77
+ prefix = env["PATH_INFO"]
78
+ method = env["REQUEST_METHOD"]
79
+ body = <<-HTML
80
+ <!DOCTYPE html>
81
+ <html>
82
+ <body>
83
+ <h1>Handler Not Found</h1>
84
+ <ul>
85
+ <li>Prefix: #{prefix.inspect}</li>
86
+ <li>Method: #{method.inspect}</li>
87
+ </ul>
88
+ </body>
89
+ </html>
90
+ HTML
91
+ ::SockJS.debug "Handler not found!"
92
+ [404, {"Content-Type" => "text/html; charset=UTF-8", "Content-Length" => body.bytesize.to_s}, [body]]
93
+ end
94
+ end
95
+
96
+ class ResetScriptName
97
+ def initialize(app)
98
+ @app = app
99
+ end
100
+
101
+ def call(env)
102
+ env["PREVIOUS_SCRIPT_NAME"], env["SCRIPT_NAME"] = env["SCRIPT_NAME"], ''
103
+ result = @app.call(env)
104
+ ensure
105
+ env["SCRIPT_NAME"] = env["PREVIOUS_SCRIPT_NAME"]
106
+ end
107
+ end
108
+
109
+ class RenderErrors
110
+ def initialize(app)
111
+ @app = app
112
+ end
113
+
114
+ def call(env)
115
+ return @app.call(env)
116
+ rescue => err
117
+ if err.respond_to? :to_html
118
+ err.to_html
119
+ else
120
+ raise
121
+ end
122
+ end
123
+ end
124
+
125
+ class ExtractServerAndSession
126
+ def initialize(app)
127
+ @app = app
128
+ end
129
+
130
+ def call(env)
131
+ match = SERVER_SESSION_REGEXP.match(env["SCRIPT_NAME"])
132
+ env["sockjs.server-id"] = match[1]
133
+ env["sockjs.session-key"] = match[2]
134
+ old_script_name, old_path_name = env["SCRIPT_NAME"], env["PATH_INFO"]
135
+ env["SCRIPT_NAME"] = env["SCRIPT_NAME"] + match[0]
136
+ env["PATH_INFO"] = match.post_match
137
+
138
+ return @app.call(env)
139
+ ensure
140
+ env["SCRIPT_NAME"], env["PATH_INFO"] = old_script_name, old_path_name
141
+ end
142
+ end
143
+
144
+ def initialize(session_class, options = nil)
145
+ #TODO refactor Connection to App
146
+ connection = ::SockJS::Connection.new(session_class, options)
147
+
148
+ options ||= {}
149
+
150
+ options = DEFAULT_OPTIONS.merge(options)
151
+
152
+ @routing = Rack::Mount::RouteSet.new do |set|
153
+ ::SockJS::Endpoint.add_routes(set, connection, options)
154
+
155
+ set.add_route(MissingHandler.new(options), {}, {}, :missing)
156
+ end
157
+
158
+ routing = @routing
159
+
160
+ @app = Rack::Builder.new do
161
+ use Rack::SockJS::ResetScriptName
162
+ use DebugRequest
163
+ run routing
164
+ end.to_app
165
+ end
166
+
167
+ attr_reader :routing
168
+
169
+ def call(env)
170
+ @app.call(env)
171
+ end
172
+ end
173
+ end