websocket-rails 0.1.5 → 0.1.6

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