sockjs 0.2.1

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 (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