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,195 @@
1
+ module WebsocketRails
2
+ module ConnectionAdapters
3
+
4
+ attr_reader :adapters
5
+ module_function :adapters
6
+
7
+ def self.register(adapter)
8
+ @adapters ||= []
9
+ @adapters.unshift adapter
10
+ end
11
+
12
+ def self.establish_connection(request, dispatcher)
13
+ adapter = adapters.detect { |a| a.accepts?(request.env) } || raise(InvalidConnectionError)
14
+ adapter.new request, dispatcher
15
+ end
16
+
17
+ class Base
18
+
19
+ include Logging
20
+
21
+ def self.accepts?(env)
22
+ false
23
+ end
24
+
25
+ def self.inherited(adapter)
26
+ ConnectionAdapters.register adapter
27
+ end
28
+
29
+ attr_reader :dispatcher, :queue, :env, :request, :data_store
30
+
31
+ # The ConnectionManager will set the connection ID when the
32
+ # connection is opened.
33
+ attr_accessor :id
34
+
35
+ def initialize(request, dispatcher)
36
+ @env = request.env.dup
37
+ @request = request
38
+ @dispatcher = dispatcher
39
+ @connected = true
40
+ @queue = EventQueue.new
41
+ @data_store = DataStore::Connection.new(self)
42
+ @delegate = WebsocketRails::DelegationController.new
43
+ @delegate.instance_variable_set(:@_env, request.env)
44
+ @delegate.instance_variable_set(:@_request, request)
45
+
46
+ start_ping_timer
47
+ end
48
+
49
+ def on_open(data=nil)
50
+ event = Event.new_on_open( self, data )
51
+ dispatch event
52
+ trigger event
53
+ end
54
+
55
+ def on_message(encoded_data)
56
+ event = Event.new_from_json( encoded_data, self )
57
+ dispatch event
58
+ end
59
+
60
+ def on_close(data=nil)
61
+ @ping_timer.try(:cancel)
62
+ dispatch Event.new_on_close( self, data )
63
+ close_connection
64
+ end
65
+
66
+ def on_error(data=nil)
67
+ event = Event.new_on_error( self, data )
68
+ dispatch event
69
+ on_close event.data
70
+ end
71
+
72
+ def enqueue(event)
73
+ @queue << event
74
+ end
75
+
76
+ def trigger(event)
77
+ send "[#{event.serialize}]"
78
+ end
79
+
80
+ def flush
81
+ message = []
82
+ @queue.flush do |event|
83
+ message << event.as_json
84
+ end
85
+ send message.to_json
86
+ end
87
+
88
+ def send_message(event_name, data = {}, options = {})
89
+ options.merge! :user_id => user_identifier, :connection => self
90
+ options[:data] = data
91
+
92
+ event = Event.new(event_name, options)
93
+ event.trigger
94
+ end
95
+
96
+ def send(message)
97
+ raise NotImplementedError, "Override this method in the connection specific adapter class"
98
+ end
99
+
100
+ def rack_response
101
+ [ -1, {}, [] ]
102
+ end
103
+
104
+ def controller_delegate
105
+ @delegate
106
+ end
107
+
108
+ def connected?
109
+ true & @connected
110
+ end
111
+
112
+ def inspect
113
+ "#<Connection::#{id}>"
114
+ end
115
+
116
+ def to_s
117
+ inspect
118
+ end
119
+
120
+ def user_connection?
121
+ not user_identifier.nil?
122
+ end
123
+
124
+ def user
125
+ return unless user_connection?
126
+ controller_delegate.current_user
127
+ end
128
+
129
+ def user_identifier
130
+ @user_identifier ||= begin
131
+ identifier = WebsocketRails.config.user_identifier
132
+
133
+ return unless current_user_responds_to?(identifier)
134
+
135
+ controller_delegate.current_user.send(identifier)
136
+ end
137
+ end
138
+
139
+ def ping_interval
140
+ @ping_interval ||= WebsocketRails.config.default_ping_interval
141
+ end
142
+
143
+ def ping_interval=(interval)
144
+ @ping_interval = interval.to_i
145
+ @ping_timer.try(:cancel)
146
+ start_ping_timer
147
+ end
148
+
149
+ private
150
+
151
+ def dispatch(event)
152
+ dispatcher.dispatch event
153
+ end
154
+
155
+ def connection_manager
156
+ dispatcher.connection_manager
157
+ end
158
+
159
+ def close_connection
160
+ @data_store.destroy!
161
+ @ping_timer.try(:cancel)
162
+ dispatcher.connection_manager.close_connection self
163
+ end
164
+
165
+ def current_user_responds_to?(identifier)
166
+ controller_delegate &&
167
+ controller_delegate.respond_to?(:current_user) &&
168
+ controller_delegate.current_user &&
169
+ controller_delegate.current_user.respond_to?(identifier)
170
+ end
171
+
172
+ attr_accessor :pong
173
+ public :pong, :pong=
174
+
175
+ def start_ping_timer
176
+ @pong = true
177
+
178
+ # Set negative interval to nil to deactivate periodic pings
179
+ if ping_interval > 0
180
+ @ping_timer = EM::PeriodicTimer.new(ping_interval) do
181
+ if pong == true
182
+ self.pong = false
183
+ ping = Event.new_on_ping self
184
+ trigger ping
185
+ else
186
+ @ping_timer.cancel
187
+ on_error
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,120 @@
1
+ module WebsocketRails
2
+ module ConnectionAdapters
3
+ class Http < Base
4
+ TERM = "\r\n".freeze
5
+ TAIL = "0#{TERM}#{TERM}".freeze
6
+ HttpHeaders = {
7
+ 'Content-Type' => 'text/json',
8
+ 'Transfer-Encoding' => 'chunked'
9
+ }
10
+
11
+ def self.accepts?(env)
12
+ true
13
+ end
14
+
15
+ attr_accessor :headers
16
+
17
+ def initialize(env,dispatcher)
18
+ super
19
+ @body = DeferrableBody.new
20
+ @headers = HttpHeaders
21
+
22
+ define_deferrable_callbacks
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
+
31
+ EM.next_tick do
32
+ @env['async.callback'].call [200, @headers, @body]
33
+ on_open
34
+ end
35
+ end
36
+
37
+ def send(message)
38
+ @body.chunk encode_chunk( message )
39
+ end
40
+
41
+ def close!
42
+ @body.close!
43
+ end
44
+
45
+ private
46
+
47
+ def define_deferrable_callbacks
48
+ @body.callback do |event|
49
+ on_close(event)
50
+ end
51
+ @body.errback do |event|
52
+ on_close(event)
53
+ end
54
+ end
55
+
56
+ # From [Rack::Stream](https://github.com/intridea/rack-stream)
57
+ def encode_chunk(c)
58
+ return nil if c.nil?
59
+ # hack to work with Rack::File for now, should not TE chunked
60
+ # things that aren't strings or respond to bytesize
61
+ c = ::File.read(c.path) if c.kind_of?(Rack::File)
62
+ size = Rack::Utils.bytesize(c)
63
+ return nil if size == 0
64
+ c.dup.force_encoding(Encoding::BINARY) if c.respond_to?(:force_encoding)
65
+ [size.to_s(16), TERM, c, TERM].join
66
+ end
67
+
68
+ # From [thin_async](https://github.com/macournoyer/thin_async)
69
+ class DeferrableBody
70
+ include EM::Deferrable
71
+
72
+ # @param chunks - object that responds to each. holds initial chunks of content
73
+ def initialize(chunks = [])
74
+ @queue = []
75
+ chunks.each {|c| chunk(c)}
76
+ end
77
+
78
+ # Enqueue a chunk of content to be flushed to stream at a later time
79
+ def chunk(*chunks)
80
+ @queue += chunks
81
+ schedule_dequeue
82
+ end
83
+
84
+ # When rack attempts to iterate over `body`, save the block,
85
+ # and execute at a later time when `@queue` has elements
86
+ def each(&blk)
87
+ @body_callback = blk
88
+ schedule_dequeue
89
+ end
90
+
91
+ def empty?
92
+ @queue.empty?
93
+ end
94
+
95
+ def close!(flush = true)
96
+ EM.next_tick {
97
+ if !flush || empty?
98
+ succeed
99
+ else
100
+ schedule_dequeue
101
+ close!(flush)
102
+ end
103
+ }
104
+ end
105
+
106
+ private
107
+
108
+ def schedule_dequeue
109
+ return unless @body_callback
110
+ EM.next_tick do
111
+ next unless c = @queue.shift
112
+ @body_callback.call(c)
113
+ schedule_dequeue unless empty?
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,36 @@
1
+ module WebsocketRails
2
+ module ConnectionAdapters
3
+ class WebSocket < Base
4
+
5
+ def self.accepts?(env)
6
+ Faye::WebSocket.websocket?( env )
7
+ end
8
+
9
+ def initialize(request, dispatcher)
10
+ super
11
+ @connection = Faye::WebSocket.new(request.env)
12
+ @connection.onmessage = method(:on_message)
13
+ @connection.onerror = method(:on_error)
14
+ @connection.onclose = method(:on_close)
15
+ @connection.rack_response
16
+ EM.next_tick do
17
+ on_open
18
+ end
19
+ end
20
+
21
+ def send(message)
22
+ @connection.send message
23
+ end
24
+
25
+ def on_message(event)
26
+ data = event.respond_to?(:data) ? event.data : event
27
+ super data
28
+ end
29
+
30
+ def close!
31
+ @connection.close
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,119 @@
1
+ require 'faye/websocket'
2
+ require 'rack'
3
+ require 'thin'
4
+
5
+ Faye::WebSocket.load_adapter('thin')
6
+
7
+ module WebsocketRails
8
+ # The +ConnectionManager+ class implements the core Rack application that handles
9
+ # incoming WebSocket connections.
10
+ class ConnectionManager
11
+
12
+ include Logging
13
+
14
+ SuccessfulResponse = [200,{'Content-Type' => 'text/plain'},['success']].freeze
15
+ BadRequestResponse = [400,{'Content-Type' => 'text/plain'},['invalid']].freeze
16
+ ExceptionResponse = [500,{'Content-Type' => 'text/plain'},['exception']].freeze
17
+
18
+ # Contains a Hash of currently open connections.
19
+ # @return [Hash]
20
+ attr_reader :connections
21
+
22
+ # Contains the {Dispatcher} instance for the active server.
23
+ # @return [Dispatcher]
24
+ attr_reader :dispatcher
25
+
26
+ # Contains the {Synchronization} instance for the active server.
27
+ # @return [Synchronization]
28
+ attr_reader :synchronization
29
+
30
+ def initialize
31
+ @connections = {}
32
+ @dispatcher = Dispatcher.new(self)
33
+
34
+ if WebsocketRails.synchronize?
35
+ EM.next_tick do
36
+ Fiber.new {
37
+ Synchronization.synchronize!
38
+ EM.add_shutdown_hook { Synchronization.shutdown! }
39
+ }.resume
40
+ end
41
+ end
42
+ end
43
+
44
+ def inspect
45
+ "websocket_rails"
46
+ end
47
+
48
+ # Primary entry point for the Rack application
49
+ def call(env)
50
+ request = ActionDispatch::Request.new(env)
51
+
52
+ if request.post?
53
+ response = parse_incoming_event(request.params)
54
+ else
55
+ response = open_connection(request)
56
+ end
57
+
58
+ response
59
+ rescue InvalidConnectionError
60
+ BadRequestResponse
61
+ end
62
+
63
+ private
64
+
65
+ def parse_incoming_event(params)
66
+ connection = find_connection_by_id(params["client_id"])
67
+ connection.on_message params["data"]
68
+ SuccessfulResponse
69
+ end
70
+
71
+ def find_connection_by_id(id)
72
+ connections[id] || raise(InvalidConnectionError)
73
+ end
74
+
75
+ # Opens a persistent connection using the appropriate {ConnectionAdapter}. Stores
76
+ # active connections in the {connections} Hash.
77
+ def open_connection(request)
78
+ connection = ConnectionAdapters.establish_connection(request, dispatcher)
79
+
80
+ assign_connection_id connection
81
+ register_user_connection connection
82
+
83
+ connections[connection.id] = connection
84
+
85
+ info "Connection opened: #{connection}"
86
+ connection.rack_response
87
+ end
88
+
89
+ def close_connection(connection)
90
+ WebsocketRails.channel_manager.unsubscribe connection
91
+ destroy_user_connection connection
92
+
93
+ connections.delete connection.id
94
+
95
+ info "Connection closed: #{connection}"
96
+ connection = nil
97
+ end
98
+ public :close_connection
99
+
100
+ def assign_connection_id(connection)
101
+ begin
102
+ id = SecureRandom.hex(10)
103
+ end while connections.has_key?(id)
104
+
105
+ connection.id = id
106
+ end
107
+
108
+ def register_user_connection(connection)
109
+ return unless connection.user_connection?
110
+ WebsocketRails.users[connection.user_identifier] = connection
111
+ end
112
+
113
+ def destroy_user_connection(connection)
114
+ return unless connection.user_connection?
115
+ WebsocketRails.users.delete(connection)
116
+ end
117
+
118
+ end
119
+ end