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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENCE +19 -0
  3. data/README.textile +118 -0
  4. data/lib/meta-state.rb +151 -0
  5. data/lib/rack/sockjs.rb +173 -0
  6. data/lib/sockjs.rb +59 -0
  7. data/lib/sockjs/callbacks.rb +19 -0
  8. data/lib/sockjs/connection.rb +45 -0
  9. data/lib/sockjs/delayed-response-body.rb +99 -0
  10. data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
  11. data/lib/sockjs/duck-punch-thin-response.rb +15 -0
  12. data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
  13. data/lib/sockjs/faye.rb +18 -0
  14. data/lib/sockjs/protocol.rb +97 -0
  15. data/lib/sockjs/servers/request.rb +137 -0
  16. data/lib/sockjs/servers/response.rb +170 -0
  17. data/lib/sockjs/session.rb +492 -0
  18. data/lib/sockjs/transport.rb +357 -0
  19. data/lib/sockjs/transports/eventsource.rb +30 -0
  20. data/lib/sockjs/transports/htmlfile.rb +73 -0
  21. data/lib/sockjs/transports/iframe.rb +68 -0
  22. data/lib/sockjs/transports/info.rb +48 -0
  23. data/lib/sockjs/transports/jsonp.rb +84 -0
  24. data/lib/sockjs/transports/websocket.rb +198 -0
  25. data/lib/sockjs/transports/welcome_screen.rb +17 -0
  26. data/lib/sockjs/transports/xhr.rb +75 -0
  27. data/lib/sockjs/version.rb +13 -0
  28. data/spec/sockjs/protocol_spec.rb +49 -0
  29. data/spec/sockjs/session_spec.rb +51 -0
  30. data/spec/sockjs/transport_spec.rb +73 -0
  31. data/spec/sockjs/transports/eventsource_spec.rb +56 -0
  32. data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
  33. data/spec/sockjs/transports/iframe_spec.rb +66 -0
  34. data/spec/sockjs/transports/jsonp_spec.rb +252 -0
  35. data/spec/sockjs/transports/websocket_spec.rb +101 -0
  36. data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
  37. data/spec/sockjs/transports/xhr_spec.rb +314 -0
  38. data/spec/sockjs/version_spec.rb +18 -0
  39. data/spec/sockjs_spec.rb +8 -0
  40. data/spec/spec_helper.rb +121 -0
  41. data/spec/support/async-test.rb +42 -0
  42. 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