websocket-rails 0.0.1 → 0.1.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.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +6 -1
- data/Gemfile.lock +28 -12
- data/MIT-LICENSE +1 -1
- data/README.md +122 -14
- data/Rakefile +18 -10
- data/bin/thin-socketrails +30 -0
- data/lib/websocket-rails.rb +11 -2
- data/lib/websocket_rails/base_controller.rb +91 -10
- data/lib/websocket_rails/connection_manager.rb +57 -27
- data/lib/websocket_rails/data_store.rb +34 -4
- data/lib/websocket_rails/dispatcher.rb +25 -46
- data/lib/websocket_rails/events.rb +53 -0
- data/lib/websocket_rails/version.rb +1 -1
- data/{test → spec}/dummy/Rakefile +0 -0
- data/{test → spec}/dummy/app/controllers/application_controller.rb +0 -0
- data/spec/dummy/app/controllers/chat_controller.rb +57 -0
- data/{test → spec}/dummy/app/helpers/application_helper.rb +0 -0
- data/{test → spec}/dummy/app/views/layouts/application.html.erb +0 -0
- data/{test → spec}/dummy/config.ru +0 -0
- data/{test → spec}/dummy/config/application.rb +1 -1
- data/{test → spec}/dummy/config/boot.rb +0 -0
- data/{test → spec}/dummy/config/database.yml +0 -0
- data/{test → spec}/dummy/config/environment.rb +0 -0
- data/{test → spec}/dummy/config/environments/development.rb +0 -0
- data/{test → spec}/dummy/config/environments/production.rb +0 -0
- data/{test → spec}/dummy/config/environments/test.rb +0 -0
- data/{test → spec}/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/spec/dummy/config/initializers/events.rb +7 -0
- data/{test → spec}/dummy/config/initializers/inflections.rb +0 -0
- data/{test → spec}/dummy/config/initializers/mime_types.rb +0 -0
- data/{test → spec}/dummy/config/initializers/secret_token.rb +0 -0
- data/{test → spec}/dummy/config/initializers/session_store.rb +0 -0
- data/{test → spec}/dummy/config/locales/en.yml +0 -0
- data/{test → spec}/dummy/config/routes.rb +0 -0
- data/{test/dummy/public/favicon.ico → spec/dummy/db/test.sqlite3} +0 -0
- data/{test/dummy/public/stylesheets/.gitkeep → spec/dummy/log/development.log} +0 -0
- data/spec/dummy/log/production.log +0 -0
- data/spec/dummy/log/server.log +0 -0
- data/spec/dummy/log/test.log +0 -0
- data/{test → spec}/dummy/public/404.html +0 -0
- data/{test → spec}/dummy/public/422.html +0 -0
- data/{test → spec}/dummy/public/500.html +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/{test → spec}/dummy/public/javascripts/application.js +0 -0
- data/{test → spec}/dummy/public/javascripts/controls.js +0 -0
- data/{test → spec}/dummy/public/javascripts/dragdrop.js +0 -0
- data/{test → spec}/dummy/public/javascripts/effects.js +0 -0
- data/{test → spec}/dummy/public/javascripts/prototype.js +0 -0
- data/{test → spec}/dummy/public/javascripts/rails.js +0 -0
- data/spec/dummy/public/stylesheets/.gitkeep +0 -0
- data/{test → spec}/dummy/script/rails +0 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/support/mock_web_socket.rb +27 -0
- data/spec/unit/connection_manager_spec.rb +111 -0
- data/spec/unit/data_store_spec.rb +15 -0
- data/spec/unit/dispatcher_spec.rb +57 -0
- data/spec/unit/events_spec.rb +70 -0
- data/websocket-rails.gemspec +2 -2
- metadata +65 -53
- data/lib/websocket_rails/extensions/common.rb +0 -11
- data/lib/websocket_rails/extensions/websocket_rack.rb +0 -55
- data/test/integration/navigation_test.rb +0 -7
- data/test/support/integration_case.rb +0 -5
- data/test/test_helper.rb +0 -22
- data/test/websocket_rails_test.rb +0 -7
@@ -1,38 +1,68 @@
|
|
1
|
+
require 'faye/websocket'
|
1
2
|
require 'rack'
|
2
|
-
require '
|
3
|
-
|
3
|
+
require 'thin'
|
4
|
+
|
4
5
|
module WebsocketRails
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
super
|
9
|
-
end
|
10
|
-
|
11
|
-
def on_open(env)
|
12
|
-
puts "Client connected\n"
|
13
|
-
@dispatcher.dispatch('client_connected',{},env)
|
14
|
-
end
|
6
|
+
# The +ConnectionManager+ class implements the core Rack application that handles
|
7
|
+
# incoming WebSocket connections.
|
8
|
+
class ConnectionManager
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
# Contains an Array of currently open Faye::WebSocket connections.
|
11
|
+
# @return [Array]
|
12
|
+
attr_accessor :connections
|
19
13
|
|
20
|
-
def
|
21
|
-
|
14
|
+
def initialize
|
15
|
+
@connections = []
|
16
|
+
@dispatcher = Dispatcher.new( self )
|
22
17
|
end
|
23
18
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
19
|
+
# Opens a new Faye::WebSocket connection using the Rack env Hash. New connections
|
20
|
+
# dispatch the 'client_connected' event through the {Dispatcher} and are then
|
21
|
+
# stored in the active {connections} Array. An Async response is returned to
|
22
|
+
# signify to the web server that the connection will remain opened. Invalid
|
23
|
+
# connections return an HTTP 400 Bad Request response to the client.
|
24
|
+
def call(env)
|
25
|
+
return invalid_connection_attempt unless Faye::WebSocket.websocket?( env )
|
26
|
+
connection = Faye::WebSocket.new( env )
|
27
|
+
|
28
|
+
puts "Client #{connection} connected\n"
|
29
|
+
@dispatcher.dispatch( 'client_connected', {}, connection )
|
30
|
+
|
31
|
+
connection.onmessage = lambda do |event|
|
32
|
+
@dispatcher.receive( event.data, connection )
|
33
|
+
end
|
34
|
+
|
35
|
+
connection.onerror = lambda do |event|
|
36
|
+
@dispatcher.dispatch( 'client_error', {}, connection )
|
37
|
+
connection.onclose
|
38
|
+
end
|
39
|
+
|
40
|
+
connection.onclose = lambda do |event|
|
41
|
+
@dispatcher.dispatch( 'client_disconnected', {}, connection )
|
42
|
+
connections.delete( connection )
|
43
|
+
|
44
|
+
puts "Client #{connection} disconnected\n"
|
45
|
+
connection = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
connections << connection
|
49
|
+
connection.rack_response
|
28
50
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
51
|
+
|
52
|
+
# Used to broadcast a message to all connected clients. This method should never
|
53
|
+
# be called directly. Instead, users should use {BaseController#broadcast_message}
|
54
|
+
# and {BaseController#send_message} in their applications.
|
55
|
+
def broadcast_message(message)
|
56
|
+
@connections.map do |connection|
|
57
|
+
connection.send message
|
58
|
+
end
|
32
59
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def invalid_connection_attempt
|
64
|
+
[400,{'Content-Type' => 'text/plain'}, ['Connection was not a valid WebSocket connection']]
|
36
65
|
end
|
66
|
+
|
37
67
|
end
|
38
68
|
end
|
@@ -1,5 +1,39 @@
|
|
1
1
|
module WebsocketRails
|
2
|
+
# Provides a convenient way to persist data between events on a per client basis. Since every
|
3
|
+
# events from every client is executed on the same instance of the controller object, instance
|
4
|
+
# variables defined in actions will be shared between clients. The {DataStore} provides a Hash
|
5
|
+
# that is private for each connected client. It is accessed through a WebsocketRails controller
|
6
|
+
# using the {BaseController.data_store} instance method.
|
7
|
+
#
|
8
|
+
# = Example Usage
|
9
|
+
# == Creating a user
|
10
|
+
# # action on ChatController called by :client_connected event
|
11
|
+
# def new_user
|
12
|
+
# # This would be overwritten when the next user joins
|
13
|
+
# @user = User.new( message[:user_name] )
|
14
|
+
#
|
15
|
+
# # This will remain private for each user
|
16
|
+
# data_store[:user] = User.new( message[:user_name] )
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# == Collecting all Users from the DataStore
|
20
|
+
# Calling the {#each} method will yield the Hash for all connected clients:
|
21
|
+
# # From your controller
|
22
|
+
# all_users = []
|
23
|
+
# data_store.each { |store| all_users << store[:user] }
|
24
|
+
# The {DataStore} also uses method_missing to provide a convenience for the above case. Calling
|
25
|
+
# +data_store.each_<key>+ from a controller where +<key>+ is the hash key that you wish to collect
|
26
|
+
# will return an Array of the values for each connected client.
|
27
|
+
# # From your controller, assuming two users have already connected
|
28
|
+
# data_store[:user] = UserThree
|
29
|
+
# data_store.each_user
|
30
|
+
# => [UserOne,UserTwo,UserThree]
|
2
31
|
class DataStore
|
32
|
+
|
33
|
+
extend Forwardable
|
34
|
+
|
35
|
+
def_delegator :@base, :client_id, :cid
|
36
|
+
|
3
37
|
def initialize(base_controller)
|
4
38
|
@base = base_controller
|
5
39
|
@data = Hash.new {|h,k| h[k] = Hash.new}
|
@@ -30,10 +64,6 @@ module WebsocketRails
|
|
30
64
|
@data[cid].delete(key)
|
31
65
|
end
|
32
66
|
|
33
|
-
def cid
|
34
|
-
@base.client_id
|
35
|
-
end
|
36
|
-
|
37
67
|
def method_missing(method, *args, &block)
|
38
68
|
if /each_(?<hash_key>\w*)/ =~ method
|
39
69
|
results = []
|
@@ -1,71 +1,50 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
3
|
module WebsocketRails
|
4
|
-
class Dispatcher
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
class Dispatcher
|
5
|
+
|
6
|
+
def self.describe_events(&block)
|
7
|
+
raise "This method has been deprecated. Please use WebsocketRails::Events.describe_events instead."
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :events
|
11
|
+
|
12
|
+
def initialize(connection_manager)
|
13
|
+
@connection_manager = connection_manager
|
14
|
+
@events = Events.new( self )
|
11
15
|
end
|
12
16
|
|
13
|
-
def receive(enc_message,
|
17
|
+
def receive(enc_message,connection)
|
14
18
|
message = JSON.parse( enc_message )
|
15
19
|
event_name = message.first
|
16
20
|
data = message.last
|
17
21
|
data['received'] = Time.now.strftime("%I:%M:%p")
|
18
|
-
dispatch( event_name, data,
|
22
|
+
dispatch( event_name, data, connection )
|
19
23
|
end
|
20
24
|
|
21
|
-
def send_message(event_name,data)
|
22
|
-
|
25
|
+
def send_message(event_name,data,connection)
|
26
|
+
connection.send encoded_message( event_name, data )
|
23
27
|
end
|
24
28
|
|
25
29
|
def broadcast_message(event_name,data)
|
26
|
-
@
|
30
|
+
@connection_manager.broadcast_message encoded_message( event_name, data )
|
27
31
|
end
|
28
32
|
|
29
|
-
def dispatch(event_name,
|
30
|
-
puts "#{event_name} is handled by #{@events[event_name.to_sym].inspect}\n\n"
|
31
|
-
message = [env['websocket.client_id'],data]
|
33
|
+
def dispatch(event_name,message,connection)
|
32
34
|
Fiber.new {
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
event_symbol = event_name.to_sym
|
36
|
+
events.routes_for(event_symbol) do |controller,method|
|
37
|
+
controller.instance_variable_set(:@_message,message)
|
38
|
+
controller.instance_variable_set(:@_connection,connection)
|
39
|
+
controller.send :execute_observers, event_symbol
|
40
|
+
controller.send method if controller.respond_to?(method)
|
39
41
|
end
|
40
42
|
}.resume
|
41
43
|
end
|
42
|
-
|
43
|
-
def close_connection
|
44
|
-
@connection.close_connection
|
45
|
-
end
|
46
|
-
|
44
|
+
|
47
45
|
def encoded_message(event_name,data)
|
48
46
|
[event_name, data].to_json
|
49
47
|
end
|
50
|
-
|
51
|
-
def subscribe(event_name,options)
|
52
|
-
klass = options[:to] || raise("Must specify a class for to: option in event route")
|
53
|
-
method = options[:with_method] || raise("Must specify a method for with_method: option in event route")
|
54
|
-
controller = klass.new
|
55
|
-
if @classes[klass].nil?
|
56
|
-
@classes[klass] = controller
|
57
|
-
controller.instance_variable_set(:@_dispatcher,self)
|
58
|
-
controller.send :initialize_session if controller.respond_to?(:initialize_session)
|
59
|
-
end
|
60
|
-
@events[event_name] << [klass,method]
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.describe_events(&block)
|
64
|
-
@@event_routes = block
|
65
|
-
end
|
66
|
-
|
67
|
-
def evaluate(&block)
|
68
|
-
instance_eval &block
|
69
|
-
end
|
48
|
+
|
70
49
|
end
|
71
50
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module WebsocketRails
|
2
|
+
# Provides a DSL for mapping client events to controller actions. A single event can be mapped to any
|
3
|
+
# number of controllers and actions. You can define your event routes by creating an +events.rb+ file in
|
4
|
+
# your application's +initializers+ directory. The DSL currently consists of a single method, {#subscribe},
|
5
|
+
# which takes a symbolized event name as the first argument, and a Hash with the controller and method
|
6
|
+
# name as the second arguments.
|
7
|
+
#
|
8
|
+
# == Example events.rb file
|
9
|
+
# # located in config/initializers/events.rb
|
10
|
+
# WebsocketRails::Events.describe_events do
|
11
|
+
# subscribe :client_connected, to: ChatController, with_method: :client_connected
|
12
|
+
# subscribe :new_user, to: ChatController, with_method: :new_user
|
13
|
+
# end
|
14
|
+
class Events
|
15
|
+
|
16
|
+
def self.describe_events(&block)
|
17
|
+
WebsocketRails.route_block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :classes, :events
|
21
|
+
|
22
|
+
def initialize(dispatcher)
|
23
|
+
@dispatcher = dispatcher
|
24
|
+
evaluate( WebsocketRails.route_block ) if WebsocketRails.route_block
|
25
|
+
end
|
26
|
+
|
27
|
+
def routes_for(event,&block)
|
28
|
+
@events[event].each do |klass,method|
|
29
|
+
controller = @classes[klass]
|
30
|
+
block.call( controller, method )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def subscribe(event_name,options)
|
35
|
+
klass = options[:to] || raise("Must specify a class for to: option in event route")
|
36
|
+
method = options[:with_method] || raise("Must specify a method for with_method: option in event route")
|
37
|
+
controller = klass.new
|
38
|
+
if @classes[klass].nil?
|
39
|
+
@classes[klass] = controller
|
40
|
+
controller.instance_variable_set(:@_dispatcher,@dispatcher)
|
41
|
+
controller.send :initialize_session if controller.respond_to?(:initialize_session)
|
42
|
+
end
|
43
|
+
@events[event_name] << [klass,method]
|
44
|
+
end
|
45
|
+
|
46
|
+
def evaluate(block)
|
47
|
+
@events = Hash.new {|h,k| h[k] = Array.new}
|
48
|
+
@classes = Hash.new
|
49
|
+
instance_eval &block
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class ChatController < WebsocketRails::BaseController
|
2
|
+
|
3
|
+
observe {
|
4
|
+
if data_store.each_user.count > 0
|
5
|
+
puts 'it worked'
|
6
|
+
end
|
7
|
+
|
8
|
+
if message_counter > 10
|
9
|
+
puts 'message counter needs to be dumped'
|
10
|
+
self.message_counter = 0
|
11
|
+
end
|
12
|
+
}
|
13
|
+
|
14
|
+
observe(:new_message) {
|
15
|
+
puts "message observer fired for #{message}"
|
16
|
+
}
|
17
|
+
|
18
|
+
attr_accessor :message_counter
|
19
|
+
|
20
|
+
def initialize_session
|
21
|
+
# perform application setup here
|
22
|
+
@message_counter = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def client_connected
|
26
|
+
# do something when a client connects
|
27
|
+
end
|
28
|
+
|
29
|
+
def new_message
|
30
|
+
puts "Message from UID: #{client_id}\n"
|
31
|
+
@message_counter += 1
|
32
|
+
broadcast_message :new_message, message
|
33
|
+
end
|
34
|
+
|
35
|
+
def new_user
|
36
|
+
puts "storing user in data store\n"
|
37
|
+
data_store[:user] = message
|
38
|
+
broadcast_user_list
|
39
|
+
end
|
40
|
+
|
41
|
+
def change_username
|
42
|
+
data_store[:user] = message
|
43
|
+
broadcast_user_list
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete_user
|
47
|
+
data_store.remove_client
|
48
|
+
broadcast_user_list
|
49
|
+
end
|
50
|
+
|
51
|
+
def broadcast_user_list
|
52
|
+
users = data_store.each_user
|
53
|
+
puts "broadcasting user list: #{users}\n"
|
54
|
+
broadcast_message :user_list, users
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,7 @@
|
|
1
|
+
WebsocketRails::Events.describe_events do
|
2
|
+
subscribe :client_connected, to: ChatController, with_method: :client_connected
|
3
|
+
subscribe :new_message, to: ChatController, with_method: :new_message
|
4
|
+
subscribe :new_user, to: ChatController, with_method: :new_user
|
5
|
+
subscribe :change_username, to: ChatController, with_method: :change_username
|
6
|
+
subscribe :client_disconnected, to: ChatController, with_method: :delete_user
|
7
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|