volt-sockjs 0.3.4.4
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.
- checksums.yaml +7 -0
- 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 +18 -0
- data/lib/sockjs/protocol.rb +97 -0
- data/lib/sockjs/servers/request.rb +137 -0
- data/lib/sockjs/servers/response.rb +170 -0
- data/lib/sockjs/session.rb +492 -0
- data/lib/sockjs/transport.rb +357 -0
- data/lib/sockjs/transports/eventsource.rb +30 -0
- data/lib/sockjs/transports/htmlfile.rb +73 -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 +198 -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 +154 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
#This is the SockJS wrapper for a Rack env hash-like. Currently it requires
|
7
|
+
#that we're running under Thin - someday we may break this out such that can
|
8
|
+
#adapt to other webservers or compatiblity layers. For now: do your SockJS
|
9
|
+
#stuff in Thin.
|
10
|
+
#
|
11
|
+
class Request
|
12
|
+
attr_reader :env
|
13
|
+
def initialize(env)
|
14
|
+
@env = env
|
15
|
+
end
|
16
|
+
|
17
|
+
# request.path_info
|
18
|
+
# => /echo/abc
|
19
|
+
def path_info
|
20
|
+
env["PATH_INFO"]
|
21
|
+
end
|
22
|
+
|
23
|
+
# request.http_method
|
24
|
+
# => "GET"
|
25
|
+
def http_method
|
26
|
+
env["REQUEST_METHOD"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def async_callback
|
30
|
+
env["async.callback"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def async_close
|
34
|
+
env["async.close"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_close(&block)
|
38
|
+
async_close.callback( &block)
|
39
|
+
async_close.errback( &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def succeed
|
43
|
+
async_close.succeed
|
44
|
+
end
|
45
|
+
|
46
|
+
def fail
|
47
|
+
async_close.fail
|
48
|
+
end
|
49
|
+
|
50
|
+
#Somehow, default inspect pulls in the whole app...
|
51
|
+
def inspect
|
52
|
+
position = data.pos
|
53
|
+
data.rewind
|
54
|
+
body = data.read
|
55
|
+
"<<#{self.class.name}: #{http_method}/#{path_info} #{body.inspect}>>"
|
56
|
+
ensure
|
57
|
+
data.pos = position
|
58
|
+
end
|
59
|
+
|
60
|
+
# request.headers["origin"]
|
61
|
+
# => http://foo.bar
|
62
|
+
def headers
|
63
|
+
@headers ||=
|
64
|
+
begin
|
65
|
+
permitted_keys = /^(CONTENT_(LENGTH|TYPE))$/
|
66
|
+
|
67
|
+
@env.reduce(Hash.new) do |headers, (key, value)|
|
68
|
+
if key.match(/^HTTP_(.+)$/) || key.match(permitted_keys)
|
69
|
+
headers[$1.downcase.tr("_", "-")] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
headers
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# request.query_string["callback"]
|
78
|
+
# => "myFn"
|
79
|
+
def query_string
|
80
|
+
@query_string ||=
|
81
|
+
begin
|
82
|
+
@env["QUERY_STRING"].split("=").each_slice(2).each_with_object({}) do |pair, buffer|
|
83
|
+
buffer[pair.first] = pair.last
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# request.cookies["JSESSIONID"]
|
90
|
+
# => "123sd"
|
91
|
+
def cookies
|
92
|
+
@cookies ||=
|
93
|
+
begin
|
94
|
+
::Rack::Request.new(@env).cookies
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# request.data.read
|
100
|
+
# => "message"
|
101
|
+
def data
|
102
|
+
@env["rack.input"]
|
103
|
+
end
|
104
|
+
HTTP_1_0 ||= "HTTP/1.0"
|
105
|
+
HTTP_VERSION ||= "version"
|
106
|
+
|
107
|
+
def http_1_0?
|
108
|
+
self.headers[HTTP_VERSION] == HTTP_1_0
|
109
|
+
end
|
110
|
+
|
111
|
+
def origin
|
112
|
+
return "*" if self.headers["origin"] == "null"
|
113
|
+
self.headers["origin"] || "*"
|
114
|
+
end
|
115
|
+
|
116
|
+
def content_type
|
117
|
+
self.headers["content-type"]
|
118
|
+
end
|
119
|
+
|
120
|
+
def callback
|
121
|
+
callback = self.query_string["callback"] || self.query_string["c"]
|
122
|
+
URI.unescape(callback) if callback
|
123
|
+
end
|
124
|
+
|
125
|
+
def keep_alive?
|
126
|
+
headers["connection"].downcase == "keep-alive"
|
127
|
+
end
|
128
|
+
|
129
|
+
def session_id
|
130
|
+
self.cookies["JSESSIONID"] || "dummy"
|
131
|
+
end
|
132
|
+
|
133
|
+
def fresh?(etag)
|
134
|
+
self.headers["if-none-match"] == etag
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'sockjs/delayed-response-body'
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
#Adapter for Thin Rack responses. It's a TODO feature to support other
|
7
|
+
#webservers and compatibility layers
|
8
|
+
class Response
|
9
|
+
extend Forwardable
|
10
|
+
attr_reader :request, :status, :headers, :body
|
11
|
+
attr_writer :status
|
12
|
+
|
13
|
+
def initialize(request, status = nil, headers = nil, &block)
|
14
|
+
# request.env["async.close"]
|
15
|
+
# ["rack.input"].closed? # it's a stream
|
16
|
+
@request, @status, @headers = request, status, headers || {}
|
17
|
+
|
18
|
+
if request.http_1_0?
|
19
|
+
SockJS.debug "Request is in HTTP/1.0, responding with HTTP/1.0"
|
20
|
+
@body = DelayedResponseBody.new
|
21
|
+
else
|
22
|
+
@body = DelayedResponseChunkedBody.new
|
23
|
+
end
|
24
|
+
|
25
|
+
@body.callback do
|
26
|
+
@request.succeed
|
27
|
+
end
|
28
|
+
|
29
|
+
@body.errback do
|
30
|
+
@request.fail
|
31
|
+
end
|
32
|
+
|
33
|
+
block.call(self) if block
|
34
|
+
|
35
|
+
set_connection_keep_alive_if_requested
|
36
|
+
end
|
37
|
+
|
38
|
+
def session=(session)
|
39
|
+
@body.session = session
|
40
|
+
end
|
41
|
+
|
42
|
+
def turn_chunking_on(headers)
|
43
|
+
headers["Transfer-Encoding"] = "chunked"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def write_head(status = nil, headers = nil)
|
48
|
+
@status = status || @status || raise("Please set the status!")
|
49
|
+
@headers = headers || @headers
|
50
|
+
|
51
|
+
if @headers["Content-Length"]
|
52
|
+
raise "You can't use Content-Length with chunking!"
|
53
|
+
end
|
54
|
+
|
55
|
+
unless @request.http_1_0? || @status == 204
|
56
|
+
turn_chunking_on(@headers)
|
57
|
+
end
|
58
|
+
|
59
|
+
SockJS.debug "Writing headers: #{@status.inspect}/#{@headers.inspect}"
|
60
|
+
@request.async_callback.call([@status, @headers, @body])
|
61
|
+
|
62
|
+
@head_written = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def head_written?
|
66
|
+
!! @head_written
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(data)
|
70
|
+
self.write_head unless self.head_written?
|
71
|
+
|
72
|
+
@last_written_at = Time.now.to_i
|
73
|
+
|
74
|
+
@body.write(data)
|
75
|
+
end
|
76
|
+
|
77
|
+
def finish(data = nil, &block)
|
78
|
+
if data
|
79
|
+
self.write(data)
|
80
|
+
else
|
81
|
+
self.write_head unless self.head_written?
|
82
|
+
end
|
83
|
+
|
84
|
+
@body.finish
|
85
|
+
end
|
86
|
+
|
87
|
+
def async?
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Time.now.to_i shows time in seconds.
|
92
|
+
def due_for_alive_check
|
93
|
+
Time.now.to_i != @last_written_at
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_status(status)
|
97
|
+
@status = status
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_header(key, value)
|
101
|
+
@headers[key] = value
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_session_id(session_id)
|
105
|
+
self.headers["Set-Cookie"] = "JSESSIONID=#{session_id}; path=/"
|
106
|
+
end
|
107
|
+
|
108
|
+
# === Helpers === #
|
109
|
+
def set_access_control(origin)
|
110
|
+
self.set_header("Access-Control-Allow-Origin", origin)
|
111
|
+
self.set_header("Access-Control-Allow-Credentials", "true")
|
112
|
+
self.set_header("Access-Control-Allow-Headers", "Content-Type Origin Accept X-Requested-With X-CSRF-Token If-Modified-Since If-None-Match Auth-User-Token Authorization Connection Cookie User-Agent")
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_cache_control
|
116
|
+
year = 31536000
|
117
|
+
time = Time.now + year
|
118
|
+
|
119
|
+
self.set_header("Cache-Control", "public, max-age=#{year}")
|
120
|
+
self.set_header("Expires", time.gmtime.to_s)
|
121
|
+
self.set_header("Access-Control-Max-Age", "1000001")
|
122
|
+
end
|
123
|
+
|
124
|
+
def set_allow_options_post
|
125
|
+
self.set_header("Allow", "OPTIONS, POST")
|
126
|
+
self.set_header("Access-Control-Allow-Methods", "OPTIONS, POST")
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_allow_options_get
|
130
|
+
self.set_header("Allow", "OPTIONS, GET")
|
131
|
+
self.set_header("Access-Control-Allow-Methods", "OPTIONS, GET")
|
132
|
+
end
|
133
|
+
|
134
|
+
def set_no_cache
|
135
|
+
self.set_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
136
|
+
end
|
137
|
+
|
138
|
+
CONTENT_TYPES ||= {
|
139
|
+
plain: "text/plain; charset=UTF-8",
|
140
|
+
html: "text/html; charset=UTF-8",
|
141
|
+
javascript: "application/javascript; charset=UTF-8",
|
142
|
+
json: "application/json; charset=UTF-8",
|
143
|
+
event_stream: "text/event-stream; charset=UTF-8"
|
144
|
+
}
|
145
|
+
|
146
|
+
def set_content_length(body)
|
147
|
+
if body && body.respond_to?(:bytesize)
|
148
|
+
self.headers["Content-Length"] = body.bytesize.to_s
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def set_content_type(symbol)
|
153
|
+
if string = CONTENT_TYPES[symbol]
|
154
|
+
self.set_header("Content-Type", string)
|
155
|
+
else
|
156
|
+
raise "No such content type: #{symbol}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def set_connection_keep_alive_if_requested
|
161
|
+
if @request.env["HTTP_CONNECTION"] && @request.env["HTTP_CONNECTION"].downcase == "keep-alive"
|
162
|
+
if @request.http_1_0?
|
163
|
+
self.set_header("Connection", "Close")
|
164
|
+
else
|
165
|
+
self.set_header("Connection", "Keep-Alive")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,492 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'meta-state'
|
4
|
+
require 'sockjs/protocol'
|
5
|
+
|
6
|
+
module SockJS
|
7
|
+
class Session < MetaState::Machine
|
8
|
+
class Consumer
|
9
|
+
def initialize(response, transport)
|
10
|
+
@response = response
|
11
|
+
@transport = transport
|
12
|
+
@total_sent_length = 0
|
13
|
+
end
|
14
|
+
attr_reader :response, :transport, :total_sent_length
|
15
|
+
|
16
|
+
#Close the *response* not the *session*
|
17
|
+
def disconnect
|
18
|
+
#WEBSCOKET shouldn't have limit of data - faye will send closing frame after 1GB
|
19
|
+
if @transport.kind_of?(SockJS::Transports::WebSocket)
|
20
|
+
@total_sent_length = 0
|
21
|
+
return
|
22
|
+
end
|
23
|
+
@response.finish if @response.respond_to?(:finish)
|
24
|
+
end
|
25
|
+
|
26
|
+
def heartbeat
|
27
|
+
transport.heartbeat_frame(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
def messages(items)
|
31
|
+
unless items.empty?
|
32
|
+
@total_sent_length += transport.messages_frame(response, items)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def closing(status, message)
|
37
|
+
transport.closing_frame(response, status, message)
|
38
|
+
end
|
39
|
+
|
40
|
+
#XXX Still not sure what this is *FOR*
|
41
|
+
def check_alive
|
42
|
+
if !@response.body.closed?
|
43
|
+
if @response.due_for_alive_check
|
44
|
+
SockJS.debug "Checking if still alive"
|
45
|
+
@response.write(@transport.empty_string)
|
46
|
+
else
|
47
|
+
puts "~ [TODO] Not checking if still alive, why?"
|
48
|
+
puts "Status: #{@status} (response.body.closed: #{@response.body.closed?})\nSession class: #{self.class}\nTransport class: #{@transport.class}\nResponse: #{@response.to_s}\n\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
state :Detached do
|
55
|
+
def on_enter
|
56
|
+
@consumer = nil
|
57
|
+
clear_all_timers
|
58
|
+
set_disconnect_timer
|
59
|
+
end
|
60
|
+
|
61
|
+
def attach_consumer(response, transport)
|
62
|
+
@consumer = Consumer.new(response, transport)
|
63
|
+
activate
|
64
|
+
transition_to :attached
|
65
|
+
after_consumer_attached
|
66
|
+
end
|
67
|
+
|
68
|
+
def detach_consumer
|
69
|
+
#XXX Not sure if this is the right behavior
|
70
|
+
close(1002,"Connection interrupted")
|
71
|
+
end
|
72
|
+
|
73
|
+
def activate
|
74
|
+
end
|
75
|
+
|
76
|
+
def suspended
|
77
|
+
end
|
78
|
+
|
79
|
+
def send(*messages)
|
80
|
+
@outbox += messages
|
81
|
+
end
|
82
|
+
|
83
|
+
def close(status = nil, message = nil)
|
84
|
+
@close_status = status
|
85
|
+
@close_message = message
|
86
|
+
transition_to(:closed)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
state :Attached do
|
91
|
+
def on_enter
|
92
|
+
@consumer.messages(@outbox)
|
93
|
+
@outbox.clear
|
94
|
+
clear_all_timers
|
95
|
+
check_content_length
|
96
|
+
set_heartbeat_timer
|
97
|
+
end
|
98
|
+
|
99
|
+
def attach_consumer(response, transport)
|
100
|
+
SockJS.debug "Session#attach_consumer: another connection still open"
|
101
|
+
transport.closing_frame(response, 2010, "Another connection still open")
|
102
|
+
close(1002, "Connection interrupted")
|
103
|
+
end
|
104
|
+
|
105
|
+
def detach_consumer
|
106
|
+
transition_to :detached
|
107
|
+
after_consumer_detached
|
108
|
+
end
|
109
|
+
|
110
|
+
def send(*messages)
|
111
|
+
@consumer.messages(messages)
|
112
|
+
check_content_length
|
113
|
+
end
|
114
|
+
|
115
|
+
def send_heartbeat
|
116
|
+
@consumer.heartbeat
|
117
|
+
end
|
118
|
+
|
119
|
+
def suspend
|
120
|
+
transition_to :suspended
|
121
|
+
end
|
122
|
+
|
123
|
+
def activate
|
124
|
+
end
|
125
|
+
|
126
|
+
def close(status = 1002, message = "Connection interrupted")
|
127
|
+
@close_status = status
|
128
|
+
@close_message = message
|
129
|
+
@consumer.closing(@close_status, @close_message)
|
130
|
+
@consumer = nil
|
131
|
+
transition_to(:closed)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
state :Suspended do
|
136
|
+
def on_enter
|
137
|
+
SockJS.debug "Session suspended - it is on hold"
|
138
|
+
suspended
|
139
|
+
end
|
140
|
+
|
141
|
+
def attach_consumer(response, transport)
|
142
|
+
SockJS.debug "Session#attach_consumer: another connection still open"
|
143
|
+
transport.closing_frame(response, 2010, "Another connection still open")
|
144
|
+
close(1002, "Connection interrupted")
|
145
|
+
end
|
146
|
+
|
147
|
+
def detach_consumer
|
148
|
+
transition_to :detached
|
149
|
+
after_consumer_detached
|
150
|
+
end
|
151
|
+
|
152
|
+
def send(*messages)
|
153
|
+
@outbox += messages
|
154
|
+
end
|
155
|
+
|
156
|
+
def send_heartbeat
|
157
|
+
@consumer.heartbeat
|
158
|
+
end
|
159
|
+
|
160
|
+
def suspend
|
161
|
+
end
|
162
|
+
|
163
|
+
def activate
|
164
|
+
SockJS.debug "Session activated - is not on hold anymore!"
|
165
|
+
transition_to :attached
|
166
|
+
activated
|
167
|
+
end
|
168
|
+
|
169
|
+
def close(status = 1002, message = "Connection interrupted")
|
170
|
+
@close_status = status
|
171
|
+
@close_message = message
|
172
|
+
@consumer.closing(@close_status, @close_message)
|
173
|
+
@consumer = nil
|
174
|
+
transition_to(:closed)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
state :Closed do
|
179
|
+
def on_enter
|
180
|
+
@close_status ||= 3000
|
181
|
+
@close_message ||= "Go away!"
|
182
|
+
clear_all_timers
|
183
|
+
set_close_timer
|
184
|
+
closed
|
185
|
+
end
|
186
|
+
|
187
|
+
def suspend
|
188
|
+
SockJS.debug "Session#suspend: connection closed!"
|
189
|
+
end
|
190
|
+
|
191
|
+
def activate
|
192
|
+
SockJS.debug "Session#activate: connection closed!"
|
193
|
+
end
|
194
|
+
|
195
|
+
def attach_consumer(response, transport)
|
196
|
+
transport.closing_frame(response, @close_status, @close_message)
|
197
|
+
end
|
198
|
+
|
199
|
+
def close(status=nil, message=nil)
|
200
|
+
#can be called from faye onclose hook
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
#### Client Code interface
|
206
|
+
|
207
|
+
# All incoming data is treated as incoming messages,
|
208
|
+
# either single json-encoded messages or an array
|
209
|
+
# of json-encoded messages, depending on transport.
|
210
|
+
def receive_message(data)
|
211
|
+
clear_timer(:disconnect)
|
212
|
+
activate
|
213
|
+
|
214
|
+
SockJS.debug "Session receiving message: #{data.inspect}"
|
215
|
+
messages = parse_json(data)
|
216
|
+
SockJS.debug "Message parsed as: #{messages.inspect}"
|
217
|
+
unless messages.empty?
|
218
|
+
@received_messages.push(*messages)
|
219
|
+
end
|
220
|
+
|
221
|
+
EM.next_tick do
|
222
|
+
run_user_app
|
223
|
+
end
|
224
|
+
|
225
|
+
set_disconnect_timer
|
226
|
+
end
|
227
|
+
|
228
|
+
def suspended?
|
229
|
+
current_state == SockJS::Session::Suspended
|
230
|
+
end
|
231
|
+
|
232
|
+
def check_content_length
|
233
|
+
if @consumer.total_sent_length >= max_permitted_content_length
|
234
|
+
SockJS.debug "Maximum content length exceeded, closing the connection."
|
235
|
+
#shouldn't be restarting connection?
|
236
|
+
@consumer.disconnect
|
237
|
+
else
|
238
|
+
SockJS.debug "Permitted content length: #{@consumer.total_sent_length} of #{max_permitted_content_length}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def run_user_app
|
243
|
+
unless @received_messages.empty?
|
244
|
+
reset_heartbeat_timer #XXX Only one point which can set hearbeat while state is closed
|
245
|
+
|
246
|
+
SockJS.debug "Executing user's SockJS app"
|
247
|
+
|
248
|
+
raise @error if @error
|
249
|
+
|
250
|
+
@received_messages.each do |message|
|
251
|
+
SockJS.debug "Executing app with message #{message.inspect}"
|
252
|
+
process_message(message)
|
253
|
+
end
|
254
|
+
@received_messages.clear
|
255
|
+
|
256
|
+
after_app_run
|
257
|
+
|
258
|
+
SockJS.debug "User's SockJS app finished"
|
259
|
+
end
|
260
|
+
rescue SockJS::CloseError => error
|
261
|
+
Protocol::ClosingFrame.new(error.status, error.message)
|
262
|
+
end
|
263
|
+
|
264
|
+
def process_message(message)
|
265
|
+
end
|
266
|
+
|
267
|
+
def opened
|
268
|
+
end
|
269
|
+
|
270
|
+
def after_app_run
|
271
|
+
end
|
272
|
+
|
273
|
+
def closed
|
274
|
+
end
|
275
|
+
|
276
|
+
def activated
|
277
|
+
end
|
278
|
+
|
279
|
+
def suspended
|
280
|
+
end
|
281
|
+
|
282
|
+
def after_consumer_attached
|
283
|
+
end
|
284
|
+
|
285
|
+
def after_consumer_detached
|
286
|
+
end
|
287
|
+
|
288
|
+
attr_accessor :disconnect_delay, :interval
|
289
|
+
attr_reader :transport, :response, :outbox, :closing_frame, :data
|
290
|
+
|
291
|
+
def initialize(connection)
|
292
|
+
super()
|
293
|
+
|
294
|
+
debug_with do |msg|
|
295
|
+
SockJS::debug(msg)
|
296
|
+
end
|
297
|
+
|
298
|
+
@connection = connection
|
299
|
+
@disconnect_delay = 5 # TODO: make this configurable.
|
300
|
+
@received_messages = []
|
301
|
+
@outbox = []
|
302
|
+
@total_sent_content_length = 0
|
303
|
+
@interval = 0.1
|
304
|
+
@closing_frame = nil
|
305
|
+
@data = {}
|
306
|
+
@alive = true
|
307
|
+
@timers = {}
|
308
|
+
end
|
309
|
+
|
310
|
+
def alive?
|
311
|
+
!!@alive
|
312
|
+
end
|
313
|
+
|
314
|
+
#XXX This is probably important - need to examine this case
|
315
|
+
def on_close
|
316
|
+
SockJS.debug "The connection has been closed on the client side (current status: #{@status})."
|
317
|
+
close_session(1002, "Connection interrupted")
|
318
|
+
end
|
319
|
+
|
320
|
+
def max_permitted_content_length
|
321
|
+
@max_permitted_content_length ||= ($DEBUG ? 4096 : 128_000)
|
322
|
+
end
|
323
|
+
|
324
|
+
def parse_json(data)
|
325
|
+
if data.empty?
|
326
|
+
return []
|
327
|
+
end
|
328
|
+
|
329
|
+
JSON.parse("[#{data}]")[0]
|
330
|
+
rescue JSON::ParserError => error
|
331
|
+
raise SockJS::InvalidJSON.new(500, "Broken JSON encoding.")
|
332
|
+
end
|
333
|
+
|
334
|
+
#Timers:
|
335
|
+
#"alive_checker" - need to check spec. Appears to check that response is
|
336
|
+
#live. Premature?
|
337
|
+
#
|
338
|
+
#"disconnect" - expires and closes the session - time without a consumer
|
339
|
+
#
|
340
|
+
#"close" - duration between closed and removed from management
|
341
|
+
#
|
342
|
+
#"heartbeat" - periodic for hb frame
|
343
|
+
|
344
|
+
#Timer actions:
|
345
|
+
|
346
|
+
def disconnect_expired
|
347
|
+
SockJS.debug "#{@disconnect_delay} has passed, firing @disconnect_timer"
|
348
|
+
close
|
349
|
+
#XXX Shouldn't destroy the session?
|
350
|
+
end
|
351
|
+
|
352
|
+
def check_response_alive
|
353
|
+
if @consumer
|
354
|
+
begin
|
355
|
+
@consumer.check_alive
|
356
|
+
rescue Exception => error
|
357
|
+
puts "==> #{error.message}"
|
358
|
+
SockJS.debug error
|
359
|
+
puts "==> #{error.message}"
|
360
|
+
on_close
|
361
|
+
@alive_checker.cancel
|
362
|
+
end
|
363
|
+
else
|
364
|
+
puts "~ [TODO] Not checking if still alive, why?"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def heartbeat_triggered
|
369
|
+
# It's better as we know for sure that
|
370
|
+
# clearing the buffer won't change it.
|
371
|
+
SockJS.debug "Sending heartbeat frame."
|
372
|
+
begin
|
373
|
+
send_heartbeat
|
374
|
+
rescue Exception => error
|
375
|
+
# Nah these exceptions are OK ... let's figure out when they occur
|
376
|
+
# and let's just not set the timer for such cases in the first place.
|
377
|
+
SockJS.debug "Exception when sending heartbeat frame: #{error.inspect}"
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
#Timer machinery
|
382
|
+
|
383
|
+
def set_timer(name, type, delay, &action)
|
384
|
+
@timers[name] ||=
|
385
|
+
begin
|
386
|
+
SockJS.debug "Setting timer: #{name} to expire after #{delay}"
|
387
|
+
type.new(delay, &action)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def clear_timer(name)
|
392
|
+
@timers[name].cancel unless @timers[name].nil?
|
393
|
+
@timers.delete(name)
|
394
|
+
end
|
395
|
+
|
396
|
+
def clear_all_timers
|
397
|
+
@timers.values.each do |timer|
|
398
|
+
timer.cancel
|
399
|
+
end
|
400
|
+
@timers.clear
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
def set_alive_timer
|
405
|
+
set_timer(:alive_check, EM::PeriodicTimer, 1) do
|
406
|
+
check_response_alive
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def reset_alive_timer
|
411
|
+
clear_timer(:alive_check)
|
412
|
+
set_alive_timer
|
413
|
+
end
|
414
|
+
|
415
|
+
def set_heartbeat_timer
|
416
|
+
if current_state == SockJS::Session::Closed
|
417
|
+
SockJS.debug "trying to setup heartbeat on closed session!"
|
418
|
+
return
|
419
|
+
end
|
420
|
+
clear_timer(:disconnect)
|
421
|
+
clear_timer(:alive)
|
422
|
+
set_timer(:heartbeat, EM::PeriodicTimer, 25) do
|
423
|
+
heartbeat_triggered
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def reset_heartbeat_timer
|
428
|
+
clear_timer(:heartbeat)
|
429
|
+
if current_state == SockJS::Session::Closed
|
430
|
+
SockJS.debug "trying to setup heartbeat on closed session!"
|
431
|
+
else
|
432
|
+
set_heartbeat_timer
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def set_disconnect_timer
|
437
|
+
set_timer(:disconnect, EM::Timer, @disconnect_delay) do
|
438
|
+
disconnect_expired
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def reset_disconnect_timer
|
443
|
+
clear_timer(:disconnect)
|
444
|
+
set_disconnect_timer
|
445
|
+
end
|
446
|
+
|
447
|
+
def set_close_timer
|
448
|
+
set_timer(:close, EM::Timer, @disconnect_delay) do
|
449
|
+
@alive = false
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def reset_close_timer
|
454
|
+
clear_timer(:close)
|
455
|
+
set_close_timer
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
class WebSocketSession < Session
|
460
|
+
attr_accessor :ws
|
461
|
+
undef :response
|
462
|
+
|
463
|
+
def send_data(frame)
|
464
|
+
if frame.nil?
|
465
|
+
raise TypeError.new("Frame must not be nil!")
|
466
|
+
end
|
467
|
+
|
468
|
+
unless frame.empty?
|
469
|
+
SockJS.debug "@ws.send(#{frame.inspect})"
|
470
|
+
@ws.send(frame)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def after_app_run
|
475
|
+
return super unless self.closing?
|
476
|
+
|
477
|
+
after_close
|
478
|
+
end
|
479
|
+
|
480
|
+
def after_close
|
481
|
+
SockJS.debug "after_close: calling #finish"
|
482
|
+
finish
|
483
|
+
|
484
|
+
SockJS.debug "after_close: closing @ws and clearing @transport."
|
485
|
+
@ws.close
|
486
|
+
@transport = nil
|
487
|
+
end
|
488
|
+
|
489
|
+
def set_alive_checker
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|