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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +32 -0
- data/Gemfile +2 -1
- data/README.md +29 -34
- data/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee +45 -0
- data/lib/assets/javascripts/websocket_rails/channel.js.coffee +34 -17
- data/lib/assets/javascripts/websocket_rails/event.js.coffee +13 -11
- data/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +44 -45
- data/lib/assets/javascripts/websocket_rails/main.js +1 -0
- data/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +20 -34
- data/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +60 -15
- data/lib/generators/websocket_rails/install/templates/websocket_rails.rb +15 -0
- data/lib/rails/config/routes.rb +1 -1
- data/lib/rails/tasks/websocket_rails.tasks +6 -2
- data/lib/websocket_rails/channel.rb +28 -2
- data/lib/websocket_rails/channel_manager.rb +16 -0
- data/lib/websocket_rails/configuration.rb +26 -1
- data/lib/websocket_rails/connection_adapters/http.rb +7 -0
- data/lib/websocket_rails/connection_adapters/web_socket.rb +3 -1
- data/lib/websocket_rails/connection_manager.rb +1 -1
- data/lib/websocket_rails/controller_factory.rb +1 -1
- data/lib/websocket_rails/event.rb +9 -2
- data/lib/websocket_rails/logging.rb +0 -1
- data/lib/websocket_rails/synchronization.rb +11 -7
- data/lib/websocket_rails/version.rb +1 -1
- data/spec/javascripts/generated/assets/abstract_connection.js +71 -0
- data/spec/javascripts/generated/assets/channel.js +58 -34
- data/spec/javascripts/generated/assets/event.js +12 -16
- data/spec/javascripts/generated/assets/http_connection.js +67 -65
- data/spec/javascripts/generated/assets/websocket_connection.js +36 -51
- data/spec/javascripts/generated/assets/websocket_rails.js +68 -21
- data/spec/javascripts/generated/specs/channel_spec.js +102 -19
- data/spec/javascripts/generated/specs/helpers.js +17 -0
- data/spec/javascripts/generated/specs/websocket_connection_spec.js +72 -19
- data/spec/javascripts/generated/specs/websocket_rails_spec.js +146 -47
- data/spec/javascripts/support/jasmine.yml +10 -2
- data/spec/javascripts/support/jasmine_helper.rb +38 -0
- data/spec/javascripts/websocket_rails/channel_spec.coffee +66 -12
- data/spec/javascripts/websocket_rails/event_spec.coffee +7 -7
- data/spec/javascripts/websocket_rails/helpers.coffee +6 -0
- data/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +53 -15
- data/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +108 -25
- data/spec/unit/base_controller_spec.rb +41 -0
- data/spec/unit/channel_manager_spec.rb +21 -0
- data/spec/unit/channel_spec.rb +43 -3
- data/spec/unit/connection_adapters/http_spec.rb +24 -3
- data/spec/unit/connection_adapters_spec.rb +2 -2
- data/spec/unit/connection_manager_spec.rb +1 -1
- data/spec/unit/event_spec.rb +25 -1
- data/spec/unit/logging_spec.rb +1 -1
- metadata +57 -67
- 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
|
-
|
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 == '
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
@dispatcher.state = 'disconnected'
|
38
|
-
@dispatcher.dispatch error_event
|
24
|
+
close: ->
|
25
|
+
@_conn.close()
|
39
26
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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]
|
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
|
-
@
|
52
|
-
@_conn.flush_queue
|
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
|
-
@
|
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',{}
|
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
|
data/lib/rails/config/routes.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Rails.application.routes.draw do
|
2
2
|
if Rails.version >= '4.0.0'
|
3
|
-
|
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
|
-
|
13
|
-
|
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 =>
|
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
|
@@ -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"]
|
66
|
+
connection = find_connection_by_id(params["client_id"])
|
67
67
|
connection.on_message params["data"]
|
68
68
|
SuccessfulResponse
|
69
69
|
end
|