websocket-rails 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +2 -1
  4. data/README.md +29 -34
  5. data/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee +45 -0
  6. data/lib/assets/javascripts/websocket_rails/channel.js.coffee +34 -17
  7. data/lib/assets/javascripts/websocket_rails/event.js.coffee +13 -11
  8. data/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +44 -45
  9. data/lib/assets/javascripts/websocket_rails/main.js +1 -0
  10. data/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +20 -34
  11. data/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +60 -15
  12. data/lib/generators/websocket_rails/install/templates/websocket_rails.rb +15 -0
  13. data/lib/rails/config/routes.rb +1 -1
  14. data/lib/rails/tasks/websocket_rails.tasks +6 -2
  15. data/lib/websocket_rails/channel.rb +28 -2
  16. data/lib/websocket_rails/channel_manager.rb +16 -0
  17. data/lib/websocket_rails/configuration.rb +26 -1
  18. data/lib/websocket_rails/connection_adapters/http.rb +7 -0
  19. data/lib/websocket_rails/connection_adapters/web_socket.rb +3 -1
  20. data/lib/websocket_rails/connection_manager.rb +1 -1
  21. data/lib/websocket_rails/controller_factory.rb +1 -1
  22. data/lib/websocket_rails/event.rb +9 -2
  23. data/lib/websocket_rails/logging.rb +0 -1
  24. data/lib/websocket_rails/synchronization.rb +11 -7
  25. data/lib/websocket_rails/version.rb +1 -1
  26. data/spec/javascripts/generated/assets/abstract_connection.js +71 -0
  27. data/spec/javascripts/generated/assets/channel.js +58 -34
  28. data/spec/javascripts/generated/assets/event.js +12 -16
  29. data/spec/javascripts/generated/assets/http_connection.js +67 -65
  30. data/spec/javascripts/generated/assets/websocket_connection.js +36 -51
  31. data/spec/javascripts/generated/assets/websocket_rails.js +68 -21
  32. data/spec/javascripts/generated/specs/channel_spec.js +102 -19
  33. data/spec/javascripts/generated/specs/helpers.js +17 -0
  34. data/spec/javascripts/generated/specs/websocket_connection_spec.js +72 -19
  35. data/spec/javascripts/generated/specs/websocket_rails_spec.js +146 -47
  36. data/spec/javascripts/support/jasmine.yml +10 -2
  37. data/spec/javascripts/support/jasmine_helper.rb +38 -0
  38. data/spec/javascripts/websocket_rails/channel_spec.coffee +66 -12
  39. data/spec/javascripts/websocket_rails/event_spec.coffee +7 -7
  40. data/spec/javascripts/websocket_rails/helpers.coffee +6 -0
  41. data/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +53 -15
  42. data/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +108 -25
  43. data/spec/unit/base_controller_spec.rb +41 -0
  44. data/spec/unit/channel_manager_spec.rb +21 -0
  45. data/spec/unit/channel_spec.rb +43 -3
  46. data/spec/unit/connection_adapters/http_spec.rb +24 -3
  47. data/spec/unit/connection_adapters_spec.rb +2 -2
  48. data/spec/unit/connection_manager_spec.rb +1 -1
  49. data/spec/unit/event_spec.rb +25 -1
  50. data/spec/unit/logging_spec.rb +1 -1
  51. metadata +57 -67
  52. data/spec/javascripts/support/jasmine_config.rb +0 -63
@@ -1,43 +1,29 @@
1
1
  ###
2
2
  WebSocket Interface for the WebSocketRails client.
3
3
  ###
4
- class WebSocketRails.WebSocketConnection
5
-
6
- constructor: (@url,@dispatcher) ->
4
+ class WebSocketRails.WebSocketConnection extends WebSocketRails.AbstractConnection
5
+ connection_type: 'websocket'
6
+
7
+ constructor: (@url, @dispatcher) ->
8
+ super
7
9
  if @url.match(/^wss?:\/\//)
8
10
  console.log "WARNING: Using connection urls with protocol specified is depricated"
9
- else if window.location.protocol == 'http:'
10
- @url = "ws://#{@url}"
11
- else
11
+ else if window.location.protocol == 'https:'
12
12
  @url = "wss://#{@url}"
13
-
14
- @message_queue = []
15
- @_conn = new WebSocket(@url)
16
- @_conn.onmessage = @on_message
17
- @_conn.onclose = @on_close
18
- @_conn.onerror = @on_error
19
-
20
- trigger: (event) =>
21
- if @dispatcher.state != 'connected'
22
- @message_queue.push event
23
13
  else
24
- @_conn.send event.serialize()
25
-
26
- on_message: (event) =>
27
- data = JSON.parse event.data
28
- @dispatcher.new_message data
29
-
30
- on_close: (event) =>
31
- close_event = new WebSocketRails.Event(['connection_closed', event])
32
- @dispatcher.state = 'disconnected'
33
- @dispatcher.dispatch close_event
14
+ @url = "ws://#{@url}"
15
+ @_conn = new WebSocket(@url)
16
+ @_conn.onmessage = (event) =>
17
+ event_data = JSON.parse event.data
18
+ @on_message(event_data)
19
+ @_conn.onclose = (event) =>
20
+ @on_close(event)
21
+ @_conn.onerror = (event) =>
22
+ @on_error(event)
34
23
 
35
- on_error: (event) =>
36
- error_event = new WebSocketRails.Event(['connection_error', event])
37
- @dispatcher.state = 'disconnected'
38
- @dispatcher.dispatch error_event
24
+ close: ->
25
+ @_conn.close()
39
26
 
40
- flush_queue: =>
41
- for event in @message_queue
42
- @_conn.send event.serialize()
43
- @message_queue = []
27
+ send_event: (event) ->
28
+ super
29
+ @_conn.send event.serialize()
@@ -18,24 +18,55 @@ Listening for new events from the server
18
18
  ###
19
19
  class @WebSocketRails
20
20
  constructor: (@url, @use_websockets = true) ->
21
- @state = 'connecting'
22
21
  @callbacks = {}
23
22
  @channels = {}
24
23
  @queue = {}
25
24
 
25
+ @connect()
26
+
27
+ connect: ->
28
+ @state = 'connecting'
29
+
26
30
  unless @supports_websockets() and @use_websockets
27
- @_conn = new WebSocketRails.HttpConnection url, @
31
+ @_conn = new WebSocketRails.HttpConnection @url, @
28
32
  else
29
- @_conn = new WebSocketRails.WebSocketConnection url, @
33
+ @_conn = new WebSocketRails.WebSocketConnection @url, @
30
34
 
31
35
  @_conn.new_message = @new_message
32
36
 
37
+ disconnect: ->
38
+ if @_conn
39
+ @_conn.close()
40
+ delete @_conn._conn
41
+ delete @_conn
42
+
43
+ @state = 'disconnected'
44
+
45
+ # Reconnects the whole connection,
46
+ # keeping the messages queue and its' connected channels.
47
+ #
48
+ # After successfull connection, this will:
49
+ # - reconnect to all channels, that were active while disconnecting
50
+ # - resend all events from which we haven't received any response yet
51
+ reconnect: =>
52
+ old_connection_id = @_conn?.connection_id
53
+
54
+ @disconnect()
55
+ @connect()
56
+
57
+ # Resend all unfinished events from the previous connection.
58
+ for id, event of @queue
59
+ if event.connection_id == old_connection_id && !event.is_result()
60
+ @trigger_event event
61
+
62
+ @reconnect_channels()
63
+
33
64
  new_message: (data) =>
34
65
  for socket_message in data
35
66
  event = new WebSocketRails.Event( socket_message )
36
67
  if event.is_result()
37
68
  @queue[event.id]?.run_callbacks(event.success, event.data)
38
- @queue[event.id] = null
69
+ delete @queue[event.id]
39
70
  else if event.is_channel()
40
71
  @dispatch_channel event
41
72
  else if event.is_ping()
@@ -48,8 +79,8 @@ class @WebSocketRails
48
79
 
49
80
  connection_established: (data) =>
50
81
  @state = 'connected'
51
- @connection_id = data.connection_id
52
- @_conn.flush_queue data.connection_id
82
+ @_conn.setConnectionId(data.connection_id)
83
+ @_conn.flush_queue()
53
84
  if @on_open?
54
85
  @on_open(data)
55
86
 
@@ -58,30 +89,30 @@ class @WebSocketRails
58
89
  @callbacks[event_name].push callback
59
90
 
60
91
  trigger: (event_name, data, success_callback, failure_callback) =>
61
- event = new WebSocketRails.Event( [event_name, data, @connection_id], success_callback, failure_callback )
62
- @queue[event.id] = event
63
- @_conn.trigger event
92
+ event = new WebSocketRails.Event( [event_name, data, @_conn?.connection_id], success_callback, failure_callback )
93
+ @trigger_event event
64
94
 
65
95
  trigger_event: (event) =>
66
96
  @queue[event.id] ?= event # Prevent replacing an event that has callbacks stored
67
- @_conn.trigger event
97
+ @_conn.trigger event if @_conn
98
+ event
68
99
 
69
100
  dispatch: (event) =>
70
101
  return unless @callbacks[event.name]?
71
102
  for callback in @callbacks[event.name]
72
103
  callback event.data
73
104
 
74
- subscribe: (channel_name) =>
105
+ subscribe: (channel_name, success_callback, failure_callback) =>
75
106
  unless @channels[channel_name]?
76
- channel = new WebSocketRails.Channel channel_name, @
107
+ channel = new WebSocketRails.Channel channel_name, @, false, success_callback, failure_callback
77
108
  @channels[channel_name] = channel
78
109
  channel
79
110
  else
80
111
  @channels[channel_name]
81
112
 
82
- subscribe_private: (channel_name) =>
113
+ subscribe_private: (channel_name, success_callback, failure_callback) =>
83
114
  unless @channels[channel_name]?
84
- channel = new WebSocketRails.Channel channel_name, @, true
115
+ channel = new WebSocketRails.Channel channel_name, @, true, success_callback, failure_callback
85
116
  @channels[channel_name] = channel
86
117
  channel
87
118
  else
@@ -100,8 +131,22 @@ class @WebSocketRails
100
131
  (typeof(WebSocket) == "function" or typeof(WebSocket) == "object")
101
132
 
102
133
  pong: =>
103
- pong = new WebSocketRails.Event( ['websocket_rails.pong',{},@connection_id] )
134
+ pong = new WebSocketRails.Event( ['websocket_rails.pong', {}, @_conn?.connection_id] )
104
135
  @_conn.trigger pong
105
136
 
106
137
  connection_stale: =>
107
138
  @state != 'connected'
139
+
140
+ # Destroy and resubscribe to all existing @channels.
141
+ reconnect_channels: ->
142
+ for name, channel of @channels
143
+ callbacks = channel._callbacks
144
+ channel.destroy()
145
+ delete @channels[name]
146
+
147
+ channel = if channel.is_private
148
+ @subscribe_private name
149
+ else
150
+ @subscribe name
151
+ channel._callbacks = callbacks
152
+ channel
@@ -22,6 +22,9 @@ WebsocketRails.setup do |config|
22
22
  # * Requires Redis.
23
23
  config.synchronize = false
24
24
 
25
+ # Prevent Thin from daemonizing (default is true)
26
+ # config.daemonize = false
27
+
25
28
  # Uncomment and edit to point to a different redis instance.
26
29
  # Will not be used unless standalone or synchronization mode
27
30
  # is enabled.
@@ -33,6 +36,13 @@ WebsocketRails.setup do |config|
33
36
  # when making it private, set the following to true.
34
37
  # config.keep_subscribers_when_private = false
35
38
 
39
+ # Set to true if you wish to broadcast channel subscriber_join and
40
+ # subscriber_part events. All subscribers of a channel will be
41
+ # notified when other clients join and part the channel. If you are
42
+ # using the UserManager, the current_user object will be sent along
43
+ # with the event.
44
+ # config.broadcast_subscriber_events = true
45
+
36
46
  # Used as the key for the WebsocketRails.users Hash. This method
37
47
  # will be called on the `current_user` object in your controller
38
48
  # if one exists. If `current_user` does not exist or does not
@@ -45,4 +55,9 @@ WebsocketRails.setup do |config|
45
55
  # jobs using the WebsocketRails.users UserManager.
46
56
  # config.user_class = User
47
57
 
58
+ # Supporting HTTP streaming on Internet Explorer versions 8 & 9
59
+ # requires CORS to be enabled for GET "/websocket" request.
60
+ # List here the origin domains allowed to perform the request.
61
+ # config.allowed_origins = ['http://localhost:3000']
62
+
48
63
  end
@@ -1,6 +1,6 @@
1
1
  Rails.application.routes.draw do
2
2
  if Rails.version >= '4.0.0'
3
- get "/websocket", :to => WebsocketRails::ConnectionManager.new
3
+ match "/websocket", :to => WebsocketRails::ConnectionManager.new, via: [:get, :post]
4
4
  else
5
5
  match "/websocket", :to => WebsocketRails::ConnectionManager.new
6
6
  end
@@ -9,8 +9,12 @@ namespace :websocket_rails do
9
9
 
10
10
  warn_if_standalone_not_enabled!
11
11
 
12
- fork do
13
- Thin::Controllers::Controller.new(options).start
12
+ if options[:daemonize]
13
+ fork do
14
+ Thin::Controllers::Controller.new(options).start
15
+ end
16
+ else
17
+ Thin::Controllers::Controller.new(options).start
14
18
  end
15
19
 
16
20
  puts "Websocket Rails Standalone Server listening on port #{options[:port]}"
@@ -3,7 +3,7 @@ module WebsocketRails
3
3
 
4
4
  include Logging
5
5
 
6
- delegate :config, :to => WebsocketRails
6
+ delegate :config, :channel_tokens, :channel_manager, :to => WebsocketRails
7
7
 
8
8
  attr_reader :name, :subscribers
9
9
 
@@ -15,17 +15,20 @@ module WebsocketRails
15
15
 
16
16
  def subscribe(connection)
17
17
  info "#{connection} subscribed to channel #{name}"
18
+ trigger 'subscriber_join', connection.user if config.broadcast_subscriber_events?
18
19
  @subscribers << connection
20
+ send_token connection
19
21
  end
20
22
 
21
23
  def unsubscribe(connection)
22
24
  return unless @subscribers.include? connection
23
25
  info "#{connection} unsubscribed from channel #{name}"
24
26
  @subscribers.delete connection
27
+ trigger 'subscriber_part', connection.user if config.broadcast_subscriber_events?
25
28
  end
26
29
 
27
30
  def trigger(event_name,data={},options={})
28
- options.merge! :channel => name
31
+ options.merge! :channel => name, :token => token
29
32
  options[:data] = data
30
33
 
31
34
  event = Event.new event_name, options
@@ -35,6 +38,7 @@ module WebsocketRails
35
38
  end
36
39
 
37
40
  def trigger_event(event)
41
+ return if event.token != token
38
42
  info "[#{name}] #{event.data.inspect}"
39
43
  send_data event
40
44
  end
@@ -50,8 +54,30 @@ module WebsocketRails
50
54
  @private
51
55
  end
52
56
 
57
+ def token
58
+ @token ||= channel_tokens[@name] ||= generate_unique_token
59
+ end
60
+
53
61
  private
54
62
 
63
+ def generate_unique_token
64
+ begin
65
+ new_token = SecureRandom.uuid
66
+ end while channel_tokens.values.include?(new_token)
67
+
68
+ new_token
69
+ end
70
+
71
+ def send_token(connection)
72
+ options = {
73
+ :channel => @name,
74
+ :data => {:token => token},
75
+ :connection => connection
76
+ }
77
+ info 'sending token'
78
+ Event.new('websocket_rails.channel_token', options).trigger
79
+ end
80
+
55
81
  def send_data(event)
56
82
  if WebsocketRails.synchronize? && event.server_token.nil?
57
83
  Synchronization.publish event
@@ -1,3 +1,5 @@
1
+ require 'redis-objects'
2
+
1
3
  module WebsocketRails
2
4
 
3
5
  class << self
@@ -10,6 +12,10 @@ module WebsocketRails
10
12
  channel_manager[channel]
11
13
  end
12
14
 
15
+ def channel_tokens
16
+ channel_manager.channel_tokens
17
+ end
18
+
13
19
  end
14
20
 
15
21
  class ChannelManager
@@ -20,6 +26,16 @@ module WebsocketRails
20
26
  @channels = {}.with_indifferent_access
21
27
  end
22
28
 
29
+ def channel_tokens
30
+ @channel_tokens ||= begin
31
+ if WebsocketRails.synchronize?
32
+ ::Redis::HashKey.new('websocket_rails.channel_tokens', Synchronization.redis)
33
+ else
34
+ {}
35
+ end
36
+ end
37
+ end
38
+
23
39
  def [](channel)
24
40
  @channels[channel] ||= Channel.new channel
25
41
  end
@@ -25,6 +25,23 @@ module WebsocketRails
25
25
  @keep_subscribers_when_private = value
26
26
  end
27
27
 
28
+ def allowed_origins
29
+ # allows the value to be string or array
30
+ [@allowed_origins].flatten.compact.uniq ||= []
31
+ end
32
+
33
+ def allowed_origins=(value)
34
+ @allowed_origins = value
35
+ end
36
+
37
+ def broadcast_subscriber_events?
38
+ @broadcast_subscriber_events ||= false
39
+ end
40
+
41
+ def broadcast_subscriber_events=(value)
42
+ @broadcast_subscriber_events = value
43
+ end
44
+
28
45
  def route_block=(routes)
29
46
  @event_routes = routes
30
47
  end
@@ -73,6 +90,14 @@ module WebsocketRails
73
90
  @log_internal_events = value
74
91
  end
75
92
 
93
+ def daemonize?
94
+ @daemonize.nil? ? true : @daemonize
95
+ end
96
+
97
+ def daemonize=(value)
98
+ @daemonize = value
99
+ end
100
+
76
101
  def synchronize
77
102
  @synchronize ||= false
78
103
  end
@@ -125,7 +150,7 @@ module WebsocketRails
125
150
  :tag => 'websocket_rails',
126
151
  :rackup => "#{Rails.root}/config.ru",
127
152
  :threaded => false,
128
- :daemonize => true,
153
+ :daemonize => daemonize?,
129
154
  :dirname => Rails.root,
130
155
  :max_persistent_conns => 1024,
131
156
  :max_conns => 1024
@@ -21,6 +21,13 @@ module WebsocketRails
21
21
 
22
22
  define_deferrable_callbacks
23
23
 
24
+ origin = "#{request.protocol}#{request.raw_host_with_port}"
25
+ @headers.merge!({'Access-Control-Allow-Origin' => origin}) if WebsocketRails.config.allowed_origins.include?(origin)
26
+ # IE < 10.0 hack
27
+ # XDomainRequest will not bubble up notifications of download progress in the first 2kb of the response
28
+ # http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx
29
+ @body.chunk(encode_chunk(" " * 2048))
30
+
24
31
  EM.next_tick do
25
32
  @env['async.callback'].call [200, @headers, @body]
26
33
  on_open
@@ -12,7 +12,9 @@ module WebsocketRails
12
12
  @connection.onmessage = method(:on_message)
13
13
  @connection.onerror = method(:on_error)
14
14
  @connection.onclose = method(:on_close)
15
- on_open
15
+ EM.next_tick do
16
+ on_open
17
+ end
16
18
  end
17
19
 
18
20
  def send(message)
@@ -63,7 +63,7 @@ module WebsocketRails
63
63
  private
64
64
 
65
65
  def parse_incoming_event(params)
66
- connection = find_connection_by_id(params["client_id"].to_i)
66
+ connection = find_connection_by_id(params["client_id"])
67
67
  connection.on_message params["data"]
68
68
  SuccessfulResponse
69
69
  end
@@ -47,7 +47,7 @@ module WebsocketRails
47
47
  end
48
48
 
49
49
  def set_action_name(controller, method)
50
- set_ivar :@_action_name, controller, method
50
+ set_ivar :@_action_name, controller, method.to_s
51
51
  end
52
52
 
53
53
  def set_ivar(ivar, object, value)