websocket-rails 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/CHANGELOG.md +22 -0
  2. data/Gemfile +4 -0
  3. data/README.md +66 -159
  4. data/Rakefile +31 -4
  5. data/bin/thin-socketrails +16 -1
  6. data/lib/assets/javascripts/websocket_rails/channel.js.coffee +23 -8
  7. data/lib/assets/javascripts/websocket_rails/event.js.coffee +40 -0
  8. data/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +18 -10
  9. data/lib/assets/javascripts/websocket_rails/main.js +1 -0
  10. data/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +15 -10
  11. data/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +41 -23
  12. data/lib/websocket-rails.rb +4 -4
  13. data/lib/websocket_rails/base_controller.rb +61 -29
  14. data/lib/websocket_rails/channel.rb +14 -5
  15. data/lib/websocket_rails/channel_manager.rb +3 -1
  16. data/lib/websocket_rails/connection_adapters.rb +34 -12
  17. data/lib/websocket_rails/connection_manager.rb +4 -0
  18. data/lib/websocket_rails/dispatcher.rb +27 -3
  19. data/lib/websocket_rails/engine.rb +2 -5
  20. data/lib/websocket_rails/event.rb +87 -42
  21. data/lib/websocket_rails/event_map.rb +70 -20
  22. data/lib/websocket_rails/event_queue.rb +4 -0
  23. data/lib/websocket_rails/internal_events.rb +21 -3
  24. data/lib/websocket_rails/logging.rb +18 -0
  25. data/lib/websocket_rails/version.rb +1 -1
  26. data/spec/dummy/log/test.log +0 -429
  27. data/spec/integration/connection_manager_spec.rb +3 -5
  28. data/spec/javascripts/generated/assets/channel.js +98 -0
  29. data/spec/javascripts/generated/assets/event.js +78 -0
  30. data/spec/javascripts/generated/assets/http_connection.js +108 -0
  31. data/spec/javascripts/generated/assets/websocket_connection.js +66 -0
  32. data/spec/javascripts/generated/assets/websocket_rails.js +180 -0
  33. data/spec/javascripts/generated/specs/channel_spec.js +66 -0
  34. data/spec/javascripts/generated/specs/event_spec.js +107 -0
  35. data/spec/javascripts/generated/specs/websocket_connection_spec.js +117 -0
  36. data/spec/javascripts/generated/specs/websocket_rails_spec.js +232 -0
  37. data/spec/javascripts/support/jasmine.yml +44 -0
  38. data/spec/javascripts/support/jasmine_config.rb +63 -0
  39. data/spec/javascripts/support/vendor/sinon-1.3.4.js +3555 -0
  40. data/spec/javascripts/websocket_rails/channel_spec.coffee +51 -0
  41. data/spec/javascripts/websocket_rails/event_spec.coffee +69 -0
  42. data/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +86 -0
  43. data/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +166 -0
  44. data/spec/support/helper_methods.rb +10 -1
  45. data/spec/unit/channel_spec.rb +28 -4
  46. data/spec/unit/connection_adapters_spec.rb +17 -0
  47. data/spec/unit/connection_manager_spec.rb +1 -1
  48. data/spec/unit/dispatcher_spec.rb +1 -1
  49. data/spec/unit/event_spec.rb +15 -11
  50. metadata +22 -4
@@ -18,9 +18,10 @@ Listening for new events from the server
18
18
  ###
19
19
  class window.WebSocketRails
20
20
  constructor: (@url, @use_websockets = true) ->
21
- @state = 'connecting'
22
- @callbacks = {}
23
- @channels = {}
21
+ @state = 'connecting'
22
+ @callbacks = {}
23
+ @channels = {}
24
+ @queue = {}
24
25
 
25
26
  unless @supports_websockets() and @use_websockets
26
27
  @_conn = new WebSocketRails.HttpConnection url, @
@@ -31,21 +32,24 @@ class window.WebSocketRails
31
32
 
32
33
  new_message: (data) =>
33
34
  for socket_message in data
34
- if socket_message.length > 2
35
- event_name = socket_message[1]
36
- message = socket_message[2]
37
- @dispatch_channel socket_message...
35
+ event = new WebSocketRails.Event( socket_message )
36
+ if event.is_result()
37
+ @queue[event.id]?.run_callbacks(event.success, event.data)
38
+ @queue[event.id] = null
39
+ else if event.is_channel()
40
+ @dispatch_channel event
41
+ else if event.is_ping()
42
+ @pong()
38
43
  else
39
- event_name = socket_message[0]
40
- message = socket_message[1]
41
- @dispatch socket_message...
44
+ @dispatch event
42
45
 
43
- if @state == 'connecting' and event_name == 'client_connected'
44
- @connection_established message
46
+ if @state == 'connecting' and event.name == 'client_connected'
47
+ @connection_established event.data
45
48
 
46
49
  connection_established: (data) =>
47
50
  @state = 'connected'
48
51
  @connection_id = data.connection_id
52
+ @_conn.flush_queue data.connection_id
49
53
  if @on_open?
50
54
  @on_open(data)
51
55
 
@@ -53,13 +57,19 @@ class window.WebSocketRails
53
57
  @callbacks[event_name] ?= []
54
58
  @callbacks[event_name].push callback
55
59
 
56
- trigger: (event_name, data) =>
57
- @_conn.trigger event_name, data, @connection_id
60
+ 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
58
64
 
59
- dispatch: (event_name, data) =>
60
- return unless @callbacks[event_name]?
61
- for callback in @callbacks[event_name]
62
- callback data
65
+ trigger_event: (event) =>
66
+ @queue[event.id] ?= event # Prevent replacing an event that has callbacks stored
67
+ @_conn.trigger event
68
+
69
+ dispatch: (event) =>
70
+ return unless @callbacks[event.name]?
71
+ for callback in @callbacks[event.name]
72
+ callback event.data
63
73
 
64
74
  subscribe: (channel_name) =>
65
75
  unless @channels[channel_name]?
@@ -69,13 +79,21 @@ class window.WebSocketRails
69
79
  else
70
80
  @channels[channel_name]
71
81
 
72
- trigger_channel: (channel, event_name, data) =>
73
- @_conn.trigger_channel channel, event_name, @connection_id
82
+ subscribe_private: (channel_name) =>
83
+ unless @channels[channel_name]?
84
+ channel = new WebSocketRails.Channel channel_name, @, true
85
+ @channels[channel_name] = channel
86
+ channel
87
+ else
88
+ @channels[channel_name]
74
89
 
75
- dispatch_channel: (channel, event_name, message) =>
76
- return unless @channels[channel]?
77
- @channels[channel].dispatch event_name, message
90
+ dispatch_channel: (event) =>
91
+ return unless @channels[event.channel]?
92
+ @channels[event.channel].dispatch event.name, event.data
78
93
 
79
94
  supports_websockets: =>
80
95
  (typeof(WebSocket) == "function" or typeof(WebSocket) == "object")
81
96
 
97
+ pong: =>
98
+ pong = new WebSocketRails.Event( ['websocket_rails.pong',{},@connection_id] )
99
+ @_conn.trigger pong
@@ -17,7 +17,10 @@ module WebsocketRails
17
17
  end
18
18
  end
19
19
 
20
- require "websocket_rails/engine"
20
+ LOG_LEVEL = :warn
21
+
22
+ require 'websocket_rails/engine'
23
+ require 'websocket_rails/logging'
21
24
  require 'websocket_rails/connection_manager'
22
25
  require 'websocket_rails/dispatcher'
23
26
  require 'websocket_rails/event'
@@ -32,9 +35,6 @@ require 'websocket_rails/connection_adapters'
32
35
  require 'websocket_rails/connection_adapters/http'
33
36
  require 'websocket_rails/connection_adapters/web_socket'
34
37
 
35
- ::Thin::Server.send( :remove_const, 'DEFAULT_TIMEOUT' )
36
- ::Thin::Server.const_set( 'DEFAULT_TIMEOUT', 0 )
37
-
38
38
  # Exceptions
39
39
  class InvalidConnectionError < StandardError
40
40
  def rack_response
@@ -2,10 +2,7 @@ require 'websocket_rails/data_store'
2
2
 
3
3
  module WebsocketRails
4
4
  # Provides controller helper methods for developing a WebsocketRails controller. Action methods
5
- # defined on a WebsocketRails controller can be mapped to events using the {Events} class.
6
- # This class should be sub classed in a user's application, similar to the ApplicationController
7
- # in a Rails application. You can create your WebsocketRails controllers in your standard Rails
8
- # controllers directory.
5
+ # defined on a WebsocketRails controller can be mapped to events using the {EventMap} class.
9
6
  #
10
7
  # == Example WebsocketRails controller
11
8
  # class ChatController < WebsocketRails::BaseController
@@ -17,8 +14,14 @@ module WebsocketRails
17
14
  #
18
15
  # It is best to use the provided {DataStore} to temporarily persist data for each client between
19
16
  # events. Read more about it in the {DataStore} documentation.
17
+ #
18
+ #
20
19
  class BaseController
21
-
20
+
21
+ def self.inherited(controller)
22
+ unloadable controller
23
+ end
24
+
22
25
  # Add observers to specific events or the controller in general. This functionality is similar
23
26
  # to the Rails before_filter methods. Observers are stored as Proc objects and have access
24
27
  # to the current controller environment.
@@ -42,80 +45,109 @@ module WebsocketRails
42
45
  @@observers[:general] << block
43
46
  end
44
47
  end
45
-
48
+
46
49
  # Stores the observer Procs for the current controller. See {observe} for details.
47
50
  @@observers = Hash.new {|h,k| h[k] = Array.new}
48
-
51
+
49
52
  def initialize
50
53
  @data_store = DataStore.new(self)
51
54
  end
52
-
53
- # Provides direct access to the Faye::WebSocket connection object for the client that
55
+
56
+ # Provides direct access to the connection object for the client that
54
57
  # initiated the event that is currently being executed.
55
58
  def connection
56
59
  @_event.connection
57
60
  end
58
-
61
+
59
62
  # The numerical ID for the client connection that initiated the event. The ID is unique
60
63
  # for each currently active connection but can not be used to associate a client between
61
- # multiple connection attempts.
64
+ # multiple connection attempts.
62
65
  def client_id
63
66
  connection.id
64
67
  end
65
-
68
+
69
+ # The {Event} object that triggered this action.
70
+ # Find the current event name with event.name
71
+ # Access the data sent with the event with event.data
72
+ # Find the event's namespace with event.namespace
73
+ def event
74
+ @_event
75
+ end
76
+
66
77
  # The current message that was passed from the client when the event was initiated. The
67
78
  # message is typically a standard ruby Hash object. See the README for more information.
68
79
  def message
69
80
  @_event.data
70
81
  end
71
82
  alias_method :data, :message
72
-
83
+
84
+ # Trigger the success callback function attached to the client event that triggered
85
+ # this action. The object passed to this method will be passed as an argument to
86
+ # the callback function on the client.
87
+ def trigger_success(data=nil)
88
+ event.success = true
89
+ event.data = data
90
+ event.trigger
91
+ end
92
+
93
+ # Trigger the failure callback function attached to the client event that triggered
94
+ # this action. The object passed to this method will be passed as an argument to
95
+ # the callback function on the client.
96
+ def trigger_failure(data=nil)
97
+ event.success = false
98
+ event.data = data
99
+ event.trigger
100
+ end
101
+
102
+ def accept_channel(data=nil)
103
+ channel_name = event.data[:channel]
104
+ WebsocketRails[channel_name].subscribe connection
105
+ trigger_success data
106
+ end
107
+
108
+ def deny_channel(data=nil)
109
+ trigger_failure data
110
+ end
111
+
73
112
  # Sends a message to the client that initiated the current event being executed. Messages
74
113
  # are serialized as JSON into a two element Array where the first element is the event
75
114
  # and the second element is the message that was passed, typically a Hash.
76
- #
77
- # # Will arrive on the client as JSON string like the following:
78
- # # ['new_message',{'message': 'new message for the client'}]
79
- # message_hash = {:message => 'new message for the client'}
80
- # send_message :new_message, message_hash
81
115
  #
82
116
  # To send an event under a namespace, add the `:namespace => :target_namespace` option.
83
117
  #
84
- # # Will arrive as: ['product.new_message',{'message': 'new message'}]
85
118
  # send_message :new_message, message_hash, :namespace => :product
86
119
  #
87
120
  # Nested namespaces can be passed as an array like the following:
88
121
  #
89
- # # Will arrive as: ['products.glasses.new',{'message': 'new message'}]
90
122
  # send_message :new, message_hash, :namespace => [:products,:glasses]
91
123
  #
92
124
  # See the {EventMap} documentation for more on mapping namespaced actions.
93
125
  def send_message(event_name, message, options={})
94
- options.merge! :connection => connection
95
- event = Event.new( event_name, message, options )
126
+ options.merge! :connection => connection, :data => message
127
+ event = Event.new( event_name, options )
96
128
  @_dispatcher.send_message event if @_dispatcher.respond_to?(:send_message)
97
129
  end
98
-
130
+
99
131
  # Broadcasts a message to all connected clients. See {#send_message} for message passing details.
100
132
  def broadcast_message(event_name, message, options={})
101
- options.merge! :connection => connection
102
- event = Event.new( event_name, message, options )
133
+ options.merge! :connection => connection, :data => message
134
+ event = Event.new( event_name, options )
103
135
  @_dispatcher.broadcast_message event if @_dispatcher.respond_to?(:broadcast_message)
104
136
  end
105
137
 
106
138
  def request
107
139
  @_request
108
140
  end
109
-
141
+
110
142
  # Provides access to the {DataStore} for the current controller. The {DataStore} provides convenience
111
143
  # methods for keeping track of data associated with active connections. See it's documentation for
112
144
  # more information.
113
145
  def data_store
114
146
  @data_store
115
147
  end
116
-
148
+
117
149
  private
118
-
150
+
119
151
  # Executes the observers that have been defined for this controller. General observers are executed
120
152
  # first and event specific observers are executed last. Each will be executed in the order that
121
153
  # they have been defined. This method is executed by the {Dispatcher}.
@@ -139,6 +171,6 @@ module WebsocketRails
139
171
  super
140
172
  end
141
173
  end
142
-
174
+
143
175
  end
144
176
  end
@@ -5,23 +5,32 @@ module WebsocketRails
5
5
 
6
6
  def initialize(channel_name)
7
7
  @subscribers = []
8
- @name = channel_name
8
+ @name = channel_name
9
+ @private = false
9
10
  end
10
11
 
11
12
  def subscribe(connection)
12
13
  @subscribers << connection
13
14
  end
14
15
 
15
- def trigger(event_name,data,options={})
16
- options.merge! :channel => name
17
- event = Event.new event_name, data, options
16
+ def trigger(event_name,data={})
17
+ data.merge! :channel => name
18
+ event = Event.new event_name, data
18
19
  send_data event
19
20
  end
20
21
 
21
22
  def trigger_event(event)
22
23
  send_data event
23
24
  end
24
-
25
+
26
+ def make_private
27
+ @private = true
28
+ end
29
+
30
+ def is_private?
31
+ @private
32
+ end
33
+
25
34
  private
26
35
 
27
36
  def send_data(event)
@@ -1,3 +1,5 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
1
3
  module WebsocketRails
2
4
 
3
5
  class << self
@@ -17,7 +19,7 @@ module WebsocketRails
17
19
  attr_reader :channels
18
20
 
19
21
  def initialize
20
- @channels = Hash.new.with_indifferent_access
22
+ @channels = HashWithIndifferentAccess.new
21
23
  end
22
24
 
23
25
  def [](channel)
@@ -16,6 +16,8 @@ module WebsocketRails
16
16
 
17
17
  class Base
18
18
 
19
+ include Logging
20
+
19
21
  def self.accepts?(env)
20
22
  false
21
23
  end
@@ -31,9 +33,11 @@ module WebsocketRails
31
33
  @request = request
32
34
  @queue = EventQueue.new
33
35
  @dispatcher = dispatcher
36
+ @connected = true
34
37
  @delegate = DelegationController.new
35
38
  @delegate.instance_variable_set(:@_env,request.env)
36
39
  @delegate.instance_variable_set(:@_request,request)
40
+ start_ping_timer
37
41
  end
38
42
 
39
43
  def on_open(data=nil)
@@ -43,7 +47,8 @@ module WebsocketRails
43
47
  end
44
48
 
45
49
  def on_message(encoded_data)
46
- dispatch Event.new_from_json( encoded_data, self )
50
+ event = Event.new_from_json( encoded_data, self )
51
+ dispatch event
47
52
  end
48
53
 
49
54
  def on_close(data=nil)
@@ -61,19 +66,25 @@ module WebsocketRails
61
66
  @queue << event
62
67
  end
63
68
 
69
+ attr_accessor :flush_scheduled
70
+
64
71
  def trigger(event)
65
- enqueue event
66
- unless flush_scheduled
67
- EM.next_tick { flush; flush_scheduled = false }
68
- flush_scheduled = true
69
- end
72
+ # Uncomment when implementing history queueing with redis
73
+ #enqueue event
74
+ #unless flush_scheduled
75
+ # EM.next_tick { flush; flush_scheduled = false }
76
+ # flush_scheduled = true
77
+ #end
78
+ send "[#{event.serialize}]"
70
79
  end
71
80
 
72
81
  def flush
82
+ count = 1
73
83
  message = "["
74
84
  @queue.flush do |event|
75
85
  message << event.serialize
76
- message << "," unless event == @queue.last
86
+ message << "," unless count == @queue.size
87
+ count += 1
77
88
  end
78
89
  message << "]"
79
90
  send message
@@ -105,13 +116,24 @@ module WebsocketRails
105
116
  dispatcher.connection_manager.close_connection self
106
117
  end
107
118
 
108
- def flush_scheduled
109
- @flush_scheduled
119
+ attr_accessor :pong
120
+ public :pong, :pong=
121
+
122
+ def start_ping_timer
123
+ @pong = true
124
+ @ping_timer = EM::PeriodicTimer.new(10) do
125
+ log "ping"
126
+ if pong == true
127
+ self.pong = false
128
+ ping = Event.new_on_ping self
129
+ trigger ping
130
+ else
131
+ @ping_timer.cancel
132
+ on_error
133
+ end
134
+ end
110
135
  end
111
136
 
112
- def flush_scheduled=(value)
113
- @flush_scheduled = value
114
- end
115
137
  end
116
138
 
117
139
  end
@@ -7,6 +7,8 @@ module WebsocketRails
7
7
  # incoming WebSocket connections.
8
8
  class ConnectionManager
9
9
 
10
+ include Logging
11
+
10
12
  SuccessfulResponse = [200,{'Content-Type' => 'text/plain'},['success']].freeze
11
13
  BadRequestResponse = [400,{'Content-Type' => 'text/plain'},['invalid']].freeze
12
14
  ExceptionResponse = [500,{'Content-Type' => 'text/plain'},['exception']].freeze
@@ -56,11 +58,13 @@ module WebsocketRails
56
58
  def open_connection(request)
57
59
  connection = ConnectionAdapters.establish_connection( request, dispatcher )
58
60
  connections << connection
61
+ log "Connection opened: #{connection}"
59
62
  connection.rack_response
60
63
  end
61
64
 
62
65
  def close_connection(connection)
63
66
  connections.delete connection
67
+ log "Connection closed: #{connection}"
64
68
  connection = nil
65
69
  end
66
70
  public :close_connection
@@ -2,6 +2,8 @@
2
2
 
3
3
  module WebsocketRails
4
4
  class Dispatcher
5
+
6
+ include Logging
5
7
 
6
8
  attr_reader :event_map, :connection_manager
7
9
 
@@ -21,6 +23,7 @@ module WebsocketRails
21
23
  end
22
24
 
23
25
  def dispatch(event)
26
+ log "Event received: #{event.name}"
24
27
  if event.is_channel?
25
28
  WebsocketRails[event.channel].trigger_event event
26
29
  else
@@ -38,6 +41,10 @@ module WebsocketRails
38
41
  end
39
42
  end
40
43
 
44
+ def reload_controllers!
45
+ @event_map.reload_controllers!
46
+ end
47
+
41
48
  private
42
49
 
43
50
  def route(event)
@@ -47,9 +54,13 @@ module WebsocketRails
47
54
  begin
48
55
  controller.instance_variable_set(:@_event,event)
49
56
  controller.send :execute_observers, event.name if controller.respond_to?(:execute_observers)
50
- controller.send method if controller.respond_to?(method)
51
- rescue Exception => e
52
- puts "Application Exception: #{e.inspect}"
57
+ result = controller.send method if controller.respond_to?(method)
58
+ rescue Exception => ex
59
+ puts ex.backtrace
60
+ puts "Application Exception: #{ex}"
61
+ event.success = false
62
+ event.data = extract_exception_data ex
63
+ event.trigger
53
64
  end
54
65
  end
55
66
  end
@@ -62,5 +73,18 @@ module WebsocketRails
62
73
  end
63
74
  end
64
75
 
76
+ def extract_exception_data(ex)
77
+ case ex
78
+ when ActiveRecord::RecordInvalid
79
+ {
80
+ :record => ex.record.attributes,
81
+ :errors => ex.record.errors,
82
+ :full_messages => ex.record.errors.full_messages
83
+ }
84
+ else
85
+ ex if ex.respond_to?(:to_json)
86
+ end
87
+ end
88
+
65
89
  end
66
90
  end
@@ -2,14 +2,11 @@ module WebsocketRails
2
2
 
3
3
  class Engine < Rails::Engine
4
4
  initializer "websocket_rails.load_app_instance_data" do |app|
5
+ paths['app/controllers'] = 'app/controllers'
5
6
  WebsocketRails.setup do |config|
6
7
  config.app_root = app.root
7
8
  end
8
9
  app.config.autoload_paths += [File.expand_path("../../lib", __FILE__)]
9
10
  end
10
-
11
- initializer "websocket_rails.load_static_assets" do |app|
12
- app.middleware.use ::ActionDispatch::Static, "#{root}/public"
13
- end
14
11
  end
15
- end
12
+ end