websocket-rails 0.6.2 → 0.7.0

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