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.
- data/LICENCE +19 -0
- data/README.textile +118 -0
- data/lib/meta-state.rb +151 -0
- data/lib/rack/sockjs.rb +173 -0
- data/lib/sockjs.rb +59 -0
- data/lib/sockjs/callbacks.rb +19 -0
- data/lib/sockjs/connection.rb +45 -0
- data/lib/sockjs/delayed-response-body.rb +99 -0
- data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
- data/lib/sockjs/duck-punch-thin-response.rb +15 -0
- data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
- data/lib/sockjs/faye.rb +15 -0
- data/lib/sockjs/protocol.rb +97 -0
- data/lib/sockjs/servers/request.rb +136 -0
- data/lib/sockjs/servers/response.rb +169 -0
- data/lib/sockjs/session.rb +388 -0
- data/lib/sockjs/transport.rb +354 -0
- data/lib/sockjs/transports/eventsource.rb +30 -0
- data/lib/sockjs/transports/htmlfile.rb +69 -0
- data/lib/sockjs/transports/iframe.rb +68 -0
- data/lib/sockjs/transports/info.rb +48 -0
- data/lib/sockjs/transports/jsonp.rb +84 -0
- data/lib/sockjs/transports/websocket.rb +166 -0
- data/lib/sockjs/transports/welcome_screen.rb +17 -0
- data/lib/sockjs/transports/xhr.rb +75 -0
- data/lib/sockjs/version.rb +13 -0
- data/spec/sockjs/protocol_spec.rb +49 -0
- data/spec/sockjs/session_spec.rb +51 -0
- data/spec/sockjs/transport_spec.rb +73 -0
- data/spec/sockjs/transports/eventsource_spec.rb +56 -0
- data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
- data/spec/sockjs/transports/iframe_spec.rb +66 -0
- data/spec/sockjs/transports/jsonp_spec.rb +252 -0
- data/spec/sockjs/transports/websocket_spec.rb +101 -0
- data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
- data/spec/sockjs/transports/xhr_spec.rb +314 -0
- data/spec/sockjs/version_spec.rb +18 -0
- data/spec/sockjs_spec.rb +8 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/support/async-test.rb +42 -0
- 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.
|
data/README.textile
ADDED
@@ -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
|
data/lib/meta-state.rb
ADDED
@@ -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
|
data/lib/rack/sockjs.rb
ADDED
@@ -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
|