hkroger-websocket-rails 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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