hkroger-websocket-rails 0.7.1

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 (120) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +328 -0
  3. data/Gemfile +27 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +237 -0
  6. data/Rakefile +72 -0
  7. data/bin/thin-socketrails +45 -0
  8. data/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee +45 -0
  9. data/lib/assets/javascripts/websocket_rails/channel.js.coffee +70 -0
  10. data/lib/assets/javascripts/websocket_rails/event.js.coffee +42 -0
  11. data/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +66 -0
  12. data/lib/assets/javascripts/websocket_rails/main.js +6 -0
  13. data/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +29 -0
  14. data/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +158 -0
  15. data/lib/config.ru +3 -0
  16. data/lib/generators/websocket_rails/install/install_generator.rb +33 -0
  17. data/lib/generators/websocket_rails/install/templates/events.rb +14 -0
  18. data/lib/generators/websocket_rails/install/templates/websocket_rails.rb +63 -0
  19. data/lib/hkroger-websocket-rails.rb +1 -0
  20. data/lib/rails/app/controllers/websocket_rails/delegation_controller.rb +13 -0
  21. data/lib/rails/config/routes.rb +7 -0
  22. data/lib/rails/tasks/websocket_rails.tasks +42 -0
  23. data/lib/spec_helpers/matchers/route_matchers.rb +65 -0
  24. data/lib/spec_helpers/matchers/trigger_matchers.rb +113 -0
  25. data/lib/spec_helpers/spec_helper_event.rb +34 -0
  26. data/lib/websocket-rails.rb +108 -0
  27. data/lib/websocket_rails/base_controller.rb +197 -0
  28. data/lib/websocket_rails/channel.rb +97 -0
  29. data/lib/websocket_rails/channel_manager.rb +55 -0
  30. data/lib/websocket_rails/configuration.rb +169 -0
  31. data/lib/websocket_rails/connection_adapters.rb +195 -0
  32. data/lib/websocket_rails/connection_adapters/http.rb +120 -0
  33. data/lib/websocket_rails/connection_adapters/web_socket.rb +36 -0
  34. data/lib/websocket_rails/connection_manager.rb +119 -0
  35. data/lib/websocket_rails/controller_factory.rb +80 -0
  36. data/lib/websocket_rails/data_store.rb +145 -0
  37. data/lib/websocket_rails/dispatcher.rb +129 -0
  38. data/lib/websocket_rails/engine.rb +26 -0
  39. data/lib/websocket_rails/event.rb +189 -0
  40. data/lib/websocket_rails/event_map.rb +184 -0
  41. data/lib/websocket_rails/event_queue.rb +33 -0
  42. data/lib/websocket_rails/internal_events.rb +37 -0
  43. data/lib/websocket_rails/logging.rb +133 -0
  44. data/lib/websocket_rails/spec_helpers.rb +3 -0
  45. data/lib/websocket_rails/synchronization.rb +182 -0
  46. data/lib/websocket_rails/user_manager.rb +276 -0
  47. data/lib/websocket_rails/version.rb +3 -0
  48. data/spec/dummy/Rakefile +7 -0
  49. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  50. data/spec/dummy/app/controllers/chat_controller.rb +53 -0
  51. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  52. data/spec/dummy/app/models/user.rb +2 -0
  53. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +45 -0
  56. data/spec/dummy/config/boot.rb +10 -0
  57. data/spec/dummy/config/database.yml +22 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +26 -0
  60. data/spec/dummy/config/environments/production.rb +49 -0
  61. data/spec/dummy/config/environments/test.rb +34 -0
  62. data/spec/dummy/config/events.rb +7 -0
  63. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  64. data/spec/dummy/config/initializers/inflections.rb +10 -0
  65. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  66. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  67. data/spec/dummy/config/initializers/session_store.rb +8 -0
  68. data/spec/dummy/config/locales/en.yml +5 -0
  69. data/spec/dummy/config/routes.rb +58 -0
  70. data/spec/dummy/db/development.sqlite3 +0 -0
  71. data/spec/dummy/db/migrate/20130902222552_create_users.rb +10 -0
  72. data/spec/dummy/db/schema.rb +23 -0
  73. data/spec/dummy/db/test.sqlite3 +0 -0
  74. data/spec/dummy/log/development.log +17 -0
  75. data/spec/dummy/log/production.log +0 -0
  76. data/spec/dummy/log/server.log +0 -0
  77. data/spec/dummy/public/404.html +26 -0
  78. data/spec/dummy/public/422.html +26 -0
  79. data/spec/dummy/public/500.html +26 -0
  80. data/spec/dummy/public/favicon.ico +0 -0
  81. data/spec/dummy/public/javascripts/application.js +2 -0
  82. data/spec/dummy/public/javascripts/controls.js +965 -0
  83. data/spec/dummy/public/javascripts/dragdrop.js +974 -0
  84. data/spec/dummy/public/javascripts/effects.js +1123 -0
  85. data/spec/dummy/public/javascripts/prototype.js +6001 -0
  86. data/spec/dummy/public/javascripts/rails.js +202 -0
  87. data/spec/dummy/script/rails +6 -0
  88. data/spec/integration/connection_manager_spec.rb +135 -0
  89. data/spec/javascripts/support/jasmine.yml +52 -0
  90. data/spec/javascripts/support/jasmine_helper.rb +38 -0
  91. data/spec/javascripts/support/vendor/sinon-1.7.1.js +4343 -0
  92. data/spec/javascripts/websocket_rails/channel_spec.coffee +112 -0
  93. data/spec/javascripts/websocket_rails/event_spec.coffee +69 -0
  94. data/spec/javascripts/websocket_rails/helpers.coffee +6 -0
  95. data/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +158 -0
  96. data/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +274 -0
  97. data/spec/spec_helper.rb +41 -0
  98. data/spec/spec_helpers/matchers/route_matchers_spec.rb +109 -0
  99. data/spec/spec_helpers/matchers/trigger_matchers_spec.rb +247 -0
  100. data/spec/spec_helpers/spec_helper_event_spec.rb +66 -0
  101. data/spec/support/helper_methods.rb +42 -0
  102. data/spec/support/mock_web_socket.rb +41 -0
  103. data/spec/unit/base_controller_spec.rb +74 -0
  104. data/spec/unit/channel_manager_spec.rb +58 -0
  105. data/spec/unit/channel_spec.rb +169 -0
  106. data/spec/unit/connection_adapters/http_spec.rb +88 -0
  107. data/spec/unit/connection_adapters/web_socket_spec.rb +30 -0
  108. data/spec/unit/connection_adapters_spec.rb +259 -0
  109. data/spec/unit/connection_manager_spec.rb +148 -0
  110. data/spec/unit/controller_factory_spec.rb +76 -0
  111. data/spec/unit/data_store_spec.rb +106 -0
  112. data/spec/unit/dispatcher_spec.rb +203 -0
  113. data/spec/unit/event_map_spec.rb +120 -0
  114. data/spec/unit/event_queue_spec.rb +36 -0
  115. data/spec/unit/event_spec.rb +181 -0
  116. data/spec/unit/logging_spec.rb +162 -0
  117. data/spec/unit/synchronization_spec.rb +150 -0
  118. data/spec/unit/target_validator_spec.rb +88 -0
  119. data/spec/unit/user_manager_spec.rb +165 -0
  120. metadata +320 -0
@@ -0,0 +1,197 @@
1
+ require "websocket_rails/data_store"
2
+ require 'abstract_controller/callbacks'
3
+
4
+ module WebsocketRails
5
+ # Provides controller helper methods for developing a WebsocketRails controller. Action methods
6
+ # defined on a WebsocketRails controller can be mapped to events using the {EventMap} class.
7
+ #
8
+ # == Example WebsocketRails controller
9
+ # class ChatController < WebsocketRails::BaseController
10
+ # # Can be mapped to the :client_connected event in the events.rb file.
11
+ # def new_user
12
+ # send_message :new_message, {:message => 'Welcome to the Chat Room!'}
13
+ # end
14
+ # end
15
+ #
16
+ # It is best to use the provided {DataStore} to temporarily persist data for each client between
17
+ # events. Read more about it in the {DataStore} documentation.
18
+ #
19
+ #
20
+ class BaseController
21
+
22
+ # We need process_action to be in a module loaded before AbstractController::Callbacks
23
+ # to get inheritance properly
24
+ module Metal
25
+ def process_action(method, event)
26
+ if respond_to?(method)
27
+ self.send(method)
28
+ else
29
+ raise EventRoutingError.new(event, self, method)
30
+ end
31
+ end
32
+ def response_body
33
+ false
34
+ end
35
+ end
36
+
37
+ include Metal
38
+ include AbstractController::Callbacks
39
+
40
+ # Tell Rails that BaseController and children can be reloaded when in
41
+ # the Development environment.
42
+ def self.inherited(controller)
43
+ unless controller.name == "WebsocketRails::InternalController" || Rails.version =~/^4/
44
+ unloadable controller
45
+ end
46
+ end
47
+
48
+ # Tell the dispatcher to use channel filtering on specific channels.
49
+ # If supplied, the :catch_all => :method will be processed for every
50
+ # event that comes into the channel(s).
51
+ #
52
+ #
53
+ # Example:
54
+ # To process events based upon the event_name inside :channel_one
55
+ #
56
+ # filter_for_channels :channel_one
57
+ #
58
+ # To process events based upon the event_name and a catch all
59
+ #
60
+ # filter_for_channels :channel_one, :catch_all => :logger_method
61
+ #
62
+ def self.filter_for_channels(*channels)
63
+ options = channels.last.is_a?(Hash) ? channels.pop : {}
64
+ channels.each do |channel|
65
+ WebsocketRails.filtered_channels[channel] = options[:catch_all].nil? ? self : [self, options[:catch_all]]
66
+ end
67
+ end
68
+
69
+ # Provides direct access to the connection object for the client that
70
+ # initiated the event that is currently being executed.
71
+ def connection
72
+ @_event.connection
73
+ end
74
+
75
+ # The numerical ID for the client connection that initiated the event. The ID is unique
76
+ # for each currently active connection but can not be used to associate a client between
77
+ # multiple connection attempts.
78
+ def client_id
79
+ connection.id
80
+ end
81
+
82
+ # The {Event} object that triggered this action.
83
+ # Find the current event name with event.name
84
+ # Access the data sent with the event with event.data
85
+ # Find the event's namespace with event.namespace
86
+ def event
87
+ @_event
88
+ end
89
+
90
+ # The current message that was passed from the client when the event was initiated. The
91
+ # message is typically a standard ruby Hash object. See the README for more information.
92
+ def message
93
+ @_event.data
94
+ end
95
+ alias_method :data, :message
96
+
97
+ # Trigger the success callback function attached to the client event that triggered
98
+ # this action. The object passed to this method will be passed as an argument to
99
+ # the callback function on the client.
100
+ def trigger_success(data=nil)
101
+ event.success = true
102
+ event.data = data
103
+ event.trigger
104
+ end
105
+
106
+ # Trigger the failure callback function attached to the client event that triggered
107
+ # this action. The object passed to this method will be passed as an argument to
108
+ # the callback function on the client.
109
+ def trigger_failure(data=nil)
110
+ event.success = false
111
+ event.data = data
112
+ event.trigger
113
+ end
114
+
115
+ def accept_channel(data=nil)
116
+ channel_name = event.data[:channel]
117
+ WebsocketRails[channel_name].subscribe connection
118
+ trigger_success data
119
+ end
120
+
121
+ def deny_channel(data=nil)
122
+ trigger_failure data
123
+ end
124
+
125
+ def stop_event_propagation!
126
+ event.propagate = false
127
+ end
128
+
129
+ # Sends a message to the client that initiated the current event being executed. Messages
130
+ # are serialized as JSON into a two element Array where the first element is the event
131
+ # and the second element is the message that was passed, typically a Hash.
132
+ #
133
+ # To send an event under a namespace, add the `:namespace => :target_namespace` option.
134
+ #
135
+ # send_message :new_message, message_hash, :namespace => :product
136
+ #
137
+ # Nested namespaces can be passed as an array like the following:
138
+ #
139
+ # send_message :new, message_hash, :namespace => [:products,:glasses]
140
+ #
141
+ # See the {EventMap} documentation for more on mapping namespaced actions.
142
+ def send_message(event_name, message, options={})
143
+ options.merge! :connection => connection, :data => message
144
+ event = Event.new( event_name, options )
145
+ @_dispatcher.send_message event if @_dispatcher.respond_to?(:send_message)
146
+ end
147
+
148
+ # Broadcasts a message to all connected clients. See {#send_message} for message passing details.
149
+ def broadcast_message(event_name, message, options={})
150
+ options.merge! :connection => connection, :data => message
151
+ event = Event.new( event_name, options )
152
+ @_dispatcher.broadcast_message event if @_dispatcher.respond_to?(:broadcast_message)
153
+ end
154
+
155
+ def request
156
+ connection.request
157
+ end
158
+
159
+ def action_name
160
+ @_action_name
161
+ end
162
+
163
+ # Provides access to the {DataStore} for the current controller. The {DataStore} provides convenience
164
+ # methods for keeping track of data associated with active connections. See it's documentation for
165
+ # more information.
166
+ def controller_store
167
+ @_controller_store
168
+ end
169
+
170
+ def connection_store
171
+ connection.data_store
172
+ end
173
+
174
+ def self.controller_name
175
+ self.name.underscore.gsub(/_controller$/,'')
176
+ end
177
+
178
+ def controller_name
179
+ self.class.controller_name
180
+ end
181
+
182
+ private
183
+
184
+ def delegate
185
+ connection.controller_delegate
186
+ end
187
+
188
+ def method_missing(method,*args,&block)
189
+ if delegate.respond_to? method
190
+ delegate.send method, *args, &block
191
+ else
192
+ super
193
+ end
194
+ end
195
+
196
+ end
197
+ end
@@ -0,0 +1,97 @@
1
+ module WebsocketRails
2
+ class Channel
3
+
4
+ include Logging
5
+
6
+ delegate :config, :channel_tokens, :channel_manager, :filtered_channels, :to => WebsocketRails
7
+
8
+ attr_reader :name, :subscribers
9
+
10
+ def initialize(channel_name)
11
+ @subscribers = []
12
+ @name = channel_name
13
+ @private = false
14
+ end
15
+
16
+ def subscribe(connection)
17
+ info "#{connection} subscribed to channel #{@name}"
18
+ trigger 'subscriber_join', connection.user if config.broadcast_subscriber_events?
19
+ @subscribers << connection
20
+ send_token connection
21
+ end
22
+
23
+ def unsubscribe(connection)
24
+ return unless @subscribers.include? connection
25
+ info "#{connection} unsubscribed from channel #{@name}"
26
+ @subscribers.delete connection
27
+ trigger 'subscriber_part', connection.user if config.broadcast_subscriber_events?
28
+ end
29
+
30
+ def trigger(event_name,data={},options={})
31
+ options.merge! :channel => @name, :token => token
32
+ options[:data] = data
33
+
34
+ event = Event.new event_name, options
35
+
36
+ info "[#{@name}] #{event.data.inspect}"
37
+ send_data event
38
+ end
39
+
40
+ def trigger_event(event)
41
+ return if event.token != token
42
+ info "[#{@name}] #{event.data.inspect}"
43
+ send_data event
44
+ end
45
+
46
+ def make_private
47
+ unless config.keep_subscribers_when_private?
48
+ @subscribers.clear
49
+ end
50
+ @private = true
51
+ end
52
+
53
+ def filter_with(controller, catch_all=nil)
54
+ filtered_channels[@name] = catch_all.nil? ? controller : [controller, catch_all]
55
+ end
56
+
57
+ def is_private?
58
+ @private
59
+ end
60
+
61
+ def token
62
+ @token ||= channel_tokens[@name] ||= generate_unique_token
63
+ end
64
+
65
+ private
66
+
67
+ def generate_unique_token
68
+ begin
69
+ new_token = SecureRandom.uuid
70
+ end while channel_tokens.values.include?(new_token)
71
+
72
+ new_token
73
+ end
74
+
75
+ def send_token(connection)
76
+ options = {
77
+ :channel => @name,
78
+ :data => {:token => token},
79
+ :connection => connection
80
+ }
81
+ info 'sending token'
82
+ Event.new('websocket_rails.channel_token', options).trigger
83
+ end
84
+
85
+ def send_data(event)
86
+ return unless event.should_propagate?
87
+ if WebsocketRails.synchronize? && event.server_token.nil?
88
+ Synchronization.publish event
89
+ end
90
+
91
+ @subscribers.each do |subscriber|
92
+ subscriber.trigger event
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,55 @@
1
+ require 'redis-objects'
2
+
3
+ module WebsocketRails
4
+
5
+ class << self
6
+
7
+ def channel_manager
8
+ @channel_manager ||= ChannelManager.new
9
+ end
10
+
11
+ def [](channel)
12
+ channel_manager[channel]
13
+ end
14
+
15
+ def channel_tokens
16
+ channel_manager.channel_tokens
17
+ end
18
+
19
+ def filtered_channels
20
+ channel_manager.filtered_channels
21
+ end
22
+
23
+ end
24
+
25
+ class ChannelManager
26
+
27
+ attr_reader :channels, :filtered_channels
28
+
29
+ def initialize
30
+ @channels = {}.with_indifferent_access
31
+ @filtered_channels = {}.with_indifferent_access
32
+ end
33
+
34
+ def channel_tokens
35
+ @channel_tokens ||= begin
36
+ if WebsocketRails.synchronize?
37
+ ::Redis::HashKey.new('websocket_rails.channel_tokens', Synchronization.redis)
38
+ else
39
+ {}
40
+ end
41
+ end
42
+ end
43
+
44
+ def [](channel)
45
+ @channels[channel] ||= Channel.new channel
46
+ end
47
+
48
+ def unsubscribe(connection)
49
+ @channels.each do |channel_name, channel|
50
+ channel.unsubscribe(connection)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,169 @@
1
+ module WebsocketRails
2
+ class Configuration
3
+
4
+ def user_identifier
5
+ @user_identifier ||= :id
6
+ end
7
+
8
+ def user_identifier=(identifier)
9
+ @user_identifier = identifier
10
+ end
11
+
12
+ def user_class
13
+ @user_class ||= User
14
+ end
15
+
16
+ def user_class=(klass)
17
+ @user_class = klass
18
+ end
19
+
20
+ def keep_subscribers_when_private?
21
+ @keep_subscribers_when_private ||= false
22
+ end
23
+
24
+ def keep_subscribers_when_private=(value)
25
+ @keep_subscribers_when_private = value
26
+ end
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
+
45
+ def route_block=(routes)
46
+ @event_routes = routes
47
+ end
48
+
49
+ def route_block
50
+ @event_routes
51
+ end
52
+
53
+ def log_level
54
+ @log_level ||= begin
55
+ case Rails.env.to_sym
56
+ when :production then :info
57
+ when :development then :debug
58
+ end
59
+ end
60
+ end
61
+
62
+ def log_level=(level)
63
+ @log_level = level
64
+ end
65
+
66
+ def logger
67
+ @logger ||= begin
68
+ logger = Logger.new(log_path)
69
+ Logging.configure(logger)
70
+ end
71
+ end
72
+
73
+ def logger=(logger)
74
+ @logger = logger
75
+ end
76
+
77
+ def log_path
78
+ @log_path ||= "#{Rails.root}/log/websocket_rails.log"
79
+ end
80
+
81
+ def log_path=(path)
82
+ @log_path = path
83
+ end
84
+
85
+ def log_internal_events?
86
+ @log_internal_events ||= false
87
+ end
88
+
89
+ def log_internal_events=(value)
90
+ @log_internal_events = value
91
+ end
92
+
93
+ def daemonize?
94
+ @daemonize.nil? ? true : @daemonize
95
+ end
96
+
97
+ def daemonize=(value)
98
+ @daemonize = value
99
+ end
100
+
101
+ def synchronize
102
+ @synchronize ||= false
103
+ end
104
+
105
+ def synchronize=(synchronize)
106
+ @synchronize = synchronize
107
+ end
108
+
109
+ def redis_options
110
+ @redis_options ||= redis_defaults
111
+ end
112
+
113
+ def redis_options=(options = {})
114
+ @redis_options = redis_defaults.merge(options)
115
+ end
116
+
117
+ def redis_defaults
118
+ {:host => '127.0.0.1', :port => 6379, :driver => :synchrony}
119
+ end
120
+
121
+ def standalone
122
+ @standalone ||= false
123
+ end
124
+
125
+ def standalone=(standalone)
126
+ @standalone = standalone
127
+ end
128
+
129
+ def standalone_port
130
+ @standalone_port ||= '3001'
131
+ end
132
+
133
+ def standalone_port=(port)
134
+ @standalone_port = port
135
+ end
136
+
137
+ def thin_options
138
+ @thin_options ||= thin_defaults
139
+ end
140
+
141
+ def thin_options=(options = {})
142
+ @thin_options = thin_defaults.merge(options)
143
+ end
144
+
145
+ def thin_defaults
146
+ {
147
+ :port => standalone_port,
148
+ :pid => "#{Rails.root}/tmp/pids/websocket_rails.pid",
149
+ :log => "#{Rails.root}/log/websocket_rails_server.log",
150
+ :tag => 'websocket_rails',
151
+ :rackup => "#{Rails.root}/config.ru",
152
+ :threaded => false,
153
+ :daemonize => daemonize?,
154
+ :dirname => Rails.root,
155
+ :max_persistent_conns => 1024,
156
+ :max_conns => 1024
157
+ }
158
+ end
159
+
160
+ def default_ping_interval
161
+ @default_ping_interval ||= 10
162
+ end
163
+
164
+ def default_ping_interval=(interval)
165
+ @default_ping_interval = interval.to_i
166
+ end
167
+
168
+ end
169
+ end