wwl-websocket-rails 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +328 -0
- data/Gemfile +27 -0
- data/MIT-LICENSE +20 -0
- data/README.md +239 -0
- data/Rakefile +72 -0
- data/bin/thin-socketrails +45 -0
- data/lib/assets/javascripts/websocket_rails/abstract_connection.js.coffee +45 -0
- data/lib/assets/javascripts/websocket_rails/channel.js.coffee +70 -0
- data/lib/assets/javascripts/websocket_rails/event.js.coffee +46 -0
- data/lib/assets/javascripts/websocket_rails/http_connection.js.coffee +66 -0
- data/lib/assets/javascripts/websocket_rails/main.js +6 -0
- data/lib/assets/javascripts/websocket_rails/websocket_connection.js.coffee +29 -0
- data/lib/assets/javascripts/websocket_rails/websocket_rails.js.coffee +158 -0
- data/lib/config.ru +3 -0
- data/lib/generators/websocket_rails/install/install_generator.rb +33 -0
- data/lib/generators/websocket_rails/install/templates/events.rb +14 -0
- data/lib/generators/websocket_rails/install/templates/websocket_rails.rb +68 -0
- data/lib/rails/app/controllers/websocket_rails/delegation_controller.rb +13 -0
- data/lib/rails/config/routes.rb +7 -0
- data/lib/rails/tasks/websocket_rails.tasks +42 -0
- data/lib/spec_helpers/matchers/route_matchers.rb +65 -0
- data/lib/spec_helpers/matchers/trigger_matchers.rb +138 -0
- data/lib/spec_helpers/spec_helper_event.rb +34 -0
- data/lib/websocket-rails.rb +108 -0
- data/lib/websocket_rails/base_controller.rb +208 -0
- data/lib/websocket_rails/channel.rb +97 -0
- data/lib/websocket_rails/channel_manager.rb +55 -0
- data/lib/websocket_rails/configuration.rb +177 -0
- data/lib/websocket_rails/connection_adapters/http.rb +120 -0
- data/lib/websocket_rails/connection_adapters/web_socket.rb +35 -0
- data/lib/websocket_rails/connection_adapters.rb +195 -0
- data/lib/websocket_rails/connection_manager.rb +119 -0
- data/lib/websocket_rails/controller_factory.rb +80 -0
- data/lib/websocket_rails/data_store.rb +145 -0
- data/lib/websocket_rails/dispatcher.rb +129 -0
- data/lib/websocket_rails/engine.rb +26 -0
- data/lib/websocket_rails/event.rb +193 -0
- data/lib/websocket_rails/event_map.rb +184 -0
- data/lib/websocket_rails/event_queue.rb +33 -0
- data/lib/websocket_rails/internal_events.rb +37 -0
- data/lib/websocket_rails/logging.rb +133 -0
- data/lib/websocket_rails/spec_helpers.rb +3 -0
- data/lib/websocket_rails/synchronization.rb +178 -0
- data/lib/websocket_rails/user_manager.rb +276 -0
- data/lib/websocket_rails/version.rb +3 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/controllers/chat_controller.rb +53 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/user.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +22 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +26 -0
- data/spec/dummy/config/environments/production.rb +49 -0
- data/spec/dummy/config/environments/test.rb +34 -0
- data/spec/dummy/config/events.rb +7 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20130902222552_create_users.rb +10 -0
- data/spec/dummy/db/schema.rb +23 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +17 -0
- data/spec/dummy/log/production.log +0 -0
- data/spec/dummy/log/server.log +0 -0
- data/spec/dummy/log/test.log +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/javascripts/application.js +2 -0
- data/spec/dummy/public/javascripts/controls.js +965 -0
- data/spec/dummy/public/javascripts/dragdrop.js +974 -0
- data/spec/dummy/public/javascripts/effects.js +1123 -0
- data/spec/dummy/public/javascripts/prototype.js +6001 -0
- data/spec/dummy/public/javascripts/rails.js +202 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/integration/connection_manager_spec.rb +135 -0
- data/spec/javascripts/support/jasmine.yml +52 -0
- data/spec/javascripts/support/jasmine_helper.rb +38 -0
- data/spec/javascripts/support/vendor/sinon-1.7.1.js +4343 -0
- data/spec/javascripts/websocket_rails/channel_spec.coffee +112 -0
- data/spec/javascripts/websocket_rails/event_spec.coffee +81 -0
- data/spec/javascripts/websocket_rails/helpers.coffee +6 -0
- data/spec/javascripts/websocket_rails/websocket_connection_spec.coffee +158 -0
- data/spec/javascripts/websocket_rails/websocket_rails_spec.coffee +273 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/spec_helpers/matchers/route_matchers_spec.rb +109 -0
- data/spec/spec_helpers/matchers/trigger_matchers_spec.rb +358 -0
- data/spec/spec_helpers/spec_helper_event_spec.rb +66 -0
- data/spec/support/helper_methods.rb +42 -0
- data/spec/support/mock_web_socket.rb +41 -0
- data/spec/unit/base_controller_spec.rb +74 -0
- data/spec/unit/channel_manager_spec.rb +58 -0
- data/spec/unit/channel_spec.rb +169 -0
- data/spec/unit/connection_adapters/http_spec.rb +88 -0
- data/spec/unit/connection_adapters/web_socket_spec.rb +30 -0
- data/spec/unit/connection_adapters_spec.rb +259 -0
- data/spec/unit/connection_manager_spec.rb +148 -0
- data/spec/unit/controller_factory_spec.rb +76 -0
- data/spec/unit/data_store_spec.rb +106 -0
- data/spec/unit/dispatcher_spec.rb +203 -0
- data/spec/unit/event_map_spec.rb +120 -0
- data/spec/unit/event_queue_spec.rb +36 -0
- data/spec/unit/event_spec.rb +181 -0
- data/spec/unit/logging_spec.rb +162 -0
- data/spec/unit/synchronization_spec.rb +150 -0
- data/spec/unit/target_validator_spec.rb +88 -0
- data/spec/unit/user_manager_spec.rb +165 -0
- metadata +320 -0
@@ -0,0 +1,184 @@
|
|
1
|
+
module WebsocketRails
|
2
|
+
# Provides a DSL for mapping client events to controller actions.
|
3
|
+
#
|
4
|
+
# == Example events.rb file
|
5
|
+
# # located in config/initializers/events.rb
|
6
|
+
# WebsocketRails::EventMap.describe do
|
7
|
+
# subscribe :client_connected, to: ChatController, with_method: :client_connected
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# A single event can be mapped to any number of controller actions.
|
11
|
+
#
|
12
|
+
# subscribe :new_message, :to => ChatController, :with_method => :rebroadcast_message
|
13
|
+
# subscribe :new_message, :to => LogController, :with_method => :log_message
|
14
|
+
#
|
15
|
+
# Events can be nested underneath namesapces.
|
16
|
+
#
|
17
|
+
# namespace :product do
|
18
|
+
# subscribe :new, :to => ProductController, :with_method => :new
|
19
|
+
# end
|
20
|
+
class EventMap
|
21
|
+
|
22
|
+
def self.describe(&block)
|
23
|
+
WebsocketRails.config.route_block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :namespace
|
27
|
+
|
28
|
+
def initialize(dispatcher)
|
29
|
+
@dispatcher = dispatcher
|
30
|
+
@namespace = DSL.new(dispatcher).evaluate WebsocketRails.config.route_block
|
31
|
+
@namespace = DSL.new(dispatcher,@namespace).evaluate InternalEvents.events
|
32
|
+
end
|
33
|
+
|
34
|
+
def routes_for(event, &block)
|
35
|
+
@namespace.routes_for event, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
# Proxy the reload_controllers! method to the global namespace.
|
39
|
+
def reload_controllers!
|
40
|
+
@namespace.reload_controllers!
|
41
|
+
end
|
42
|
+
|
43
|
+
# Provides the DSL methods available to the Event routes file
|
44
|
+
class DSL
|
45
|
+
|
46
|
+
def initialize(dispatcher,namespace=nil)
|
47
|
+
if namespace
|
48
|
+
@namespace = namespace
|
49
|
+
else
|
50
|
+
@namespace = Namespace.new :global, dispatcher
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def evaluate(route_block)
|
55
|
+
instance_eval &route_block unless route_block.nil?
|
56
|
+
@namespace
|
57
|
+
end
|
58
|
+
|
59
|
+
def subscribe(event_name,options)
|
60
|
+
@namespace.store event_name, options
|
61
|
+
end
|
62
|
+
|
63
|
+
def namespace(new_namespace,&block)
|
64
|
+
@namespace = @namespace.find_or_create new_namespace
|
65
|
+
instance_eval &block if block.present?
|
66
|
+
@namespace = @namespace.parent
|
67
|
+
end
|
68
|
+
|
69
|
+
def private_channel(channel)
|
70
|
+
WebsocketRails[channel].make_private
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
# Stores route map for nested namespaces
|
76
|
+
class Namespace
|
77
|
+
|
78
|
+
include Logging
|
79
|
+
|
80
|
+
attr_reader :name, :controllers, :actions, :namespaces, :parent
|
81
|
+
|
82
|
+
def initialize(name,dispatcher,parent=nil)
|
83
|
+
@name = name
|
84
|
+
@parent = parent
|
85
|
+
@dispatcher = dispatcher
|
86
|
+
@actions = Hash.new {|h,k| h[k] = Array.new}
|
87
|
+
@controllers = Hash.new
|
88
|
+
@namespaces = Hash.new
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_or_create(namespace)
|
92
|
+
unless child = namespaces[namespace]
|
93
|
+
child = Namespace.new namespace, @dispatcher, self
|
94
|
+
namespaces[namespace] = child
|
95
|
+
end
|
96
|
+
child
|
97
|
+
end
|
98
|
+
|
99
|
+
# Stores controller/action pairs for events subscribed under
|
100
|
+
# this namespace.
|
101
|
+
def store(event_name,options)
|
102
|
+
klass, action = TargetValidator.validate_target options
|
103
|
+
actions[event_name] << [klass,action]
|
104
|
+
end
|
105
|
+
|
106
|
+
# Iterates through the namespace tree and yields all
|
107
|
+
# controller/action pairs stored for the target event.
|
108
|
+
def routes_for(event, event_namespace=nil, &block)
|
109
|
+
|
110
|
+
# Grab the first level namespace from the namespace array
|
111
|
+
# and remove it from the copy.
|
112
|
+
event_namespace = copy_event_namespace( event, event_namespace ) || return
|
113
|
+
namespace = event_namespace.shift
|
114
|
+
|
115
|
+
# If the namespace matches the current namespace and we are
|
116
|
+
# at the last namespace level, yield any controller/action
|
117
|
+
# pairs for this event.
|
118
|
+
#
|
119
|
+
# If the namespace does not match, search the list of child
|
120
|
+
# namespaces stored at this level for a match and delegate
|
121
|
+
# to it's #routes_for method, passing along the current
|
122
|
+
# copy of the event's namespace array.
|
123
|
+
if namespace == @name and event_namespace.empty?
|
124
|
+
actions[event.name].each do |klass,action|
|
125
|
+
block.call klass, action
|
126
|
+
end
|
127
|
+
else
|
128
|
+
child_namespace = event_namespace.first
|
129
|
+
child = namespaces[child_namespace]
|
130
|
+
child.routes_for event, event_namespace, &block unless child.nil?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def copy_event_namespace(event, namespace=nil)
|
137
|
+
namespace = event.namespace.dup if namespace.nil?
|
138
|
+
namespace
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
class TargetValidator
|
146
|
+
|
147
|
+
# Parses the target and extracts controller/action pair or raises an error if target is invalid
|
148
|
+
def self.validate_target(target)
|
149
|
+
case target
|
150
|
+
when Hash
|
151
|
+
validate_hash_target target
|
152
|
+
when String
|
153
|
+
validate_string_target target
|
154
|
+
else
|
155
|
+
raise('Must specify the event target either as a string product#new_product or as a Hash to: ProductController, with_method: :new_product')
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
# Parses the target as a Hash, expecting keys to: and with_method:
|
162
|
+
def self.validate_hash_target(target)
|
163
|
+
klass = target[:to] || raise("Must specify a class for to: option in event route")
|
164
|
+
action = target[:with_method] || raise("Must specify a method for with_method: option in event route")
|
165
|
+
[klass, action]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Parses the target as a String, expecting it to be in the format "product#new_product"
|
169
|
+
def self.validate_string_target(target)
|
170
|
+
strings = target.split('#')
|
171
|
+
raise('The string must be in a format like product#new_product') unless strings.count == 2
|
172
|
+
klass = constantize_controller strings[0]
|
173
|
+
action = strings[1].to_sym
|
174
|
+
[klass, action]
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.constantize_controller(controller_string)
|
178
|
+
strings = (controller_string << '_controller').split('/')
|
179
|
+
strings.map{|string| string.camelize}.join('::').constantize
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module WebsocketRails
|
2
|
+
class EventQueue
|
3
|
+
|
4
|
+
attr_reader :queue
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@queue = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def enqueue(event)
|
11
|
+
@queue << event
|
12
|
+
end
|
13
|
+
alias :<< :enqueue
|
14
|
+
|
15
|
+
def last
|
16
|
+
@queue.last
|
17
|
+
end
|
18
|
+
|
19
|
+
def size
|
20
|
+
@queue.size
|
21
|
+
end
|
22
|
+
|
23
|
+
def flush(&block)
|
24
|
+
unless block.nil?
|
25
|
+
@queue.each do |item|
|
26
|
+
block.call item
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@queue = []
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module WebsocketRails
|
2
|
+
class InternalEvents
|
3
|
+
def self.events
|
4
|
+
Proc.new do
|
5
|
+
namespace :websocket_rails do
|
6
|
+
subscribe :pong, :to => InternalController, :with_method => :do_pong
|
7
|
+
subscribe :subscribe, :to => InternalController, :with_method => :subscribe_to_channel
|
8
|
+
subscribe :unsubscribe, :to => InternalController, :with_method => :unsubscribe_to_channel
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class InternalController < BaseController
|
15
|
+
include Logging
|
16
|
+
|
17
|
+
def subscribe_to_channel
|
18
|
+
channel_name = event.data[:channel]
|
19
|
+
unless WebsocketRails[channel_name].is_private?
|
20
|
+
WebsocketRails[channel_name].subscribe connection
|
21
|
+
trigger_success
|
22
|
+
else
|
23
|
+
trigger_failure( { :reason => "channel is private", :hint => "use subscribe_private instead." } )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def unsubscribe_to_channel
|
28
|
+
channel_name = event.data[:channel]
|
29
|
+
WebsocketRails[channel_name].unsubscribe connection
|
30
|
+
trigger_success
|
31
|
+
end
|
32
|
+
|
33
|
+
def do_pong
|
34
|
+
connection.pong = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'active_support/core_ext/hash'
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'bigdecimal/util'
|
5
|
+
|
6
|
+
module WebsocketRails
|
7
|
+
module Logging
|
8
|
+
# Logging module heavily influenced by Travis-Support library
|
9
|
+
|
10
|
+
LOGGABLE_DATA = [
|
11
|
+
String,
|
12
|
+
Hash,
|
13
|
+
ActiveSupport::HashWithIndifferentAccess
|
14
|
+
]
|
15
|
+
|
16
|
+
ANSI = {
|
17
|
+
:red => 31,
|
18
|
+
:green => 32,
|
19
|
+
:yellow => 33,
|
20
|
+
:cyan => 36
|
21
|
+
}
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def configure(logger)
|
25
|
+
logger.tap do
|
26
|
+
logger.formatter = proc { |*args| Format.format(*args) }
|
27
|
+
logger.level = Logger.const_get(log_level.to_s.upcase)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def log_level
|
32
|
+
WebsocketRails.config.log_level || :debug
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
delegate :logger, :to => WebsocketRails
|
37
|
+
|
38
|
+
[:fatal, :error, :warn, :info, :debug].each do |level|
|
39
|
+
define_method(level) do |*args|
|
40
|
+
message, options = *args
|
41
|
+
log(level, message, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def log(level, message, options = {})
|
46
|
+
message.chomp.split("\n").each do |line|
|
47
|
+
logger.send(level, wrap(level, self, line, options || {}))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def log_event_start(event)
|
52
|
+
message = "Started Event: #{event.encoded_name}\n"
|
53
|
+
message << "#{colorize(:cyan, "Name:")} #{event.encoded_name}\n"
|
54
|
+
message << "#{colorize(:cyan, "Data:")} #{event.data.inspect}\n" if log_data?(event)
|
55
|
+
message << "#{colorize(:cyan, "Connection:")} #{event.connection}\n\n"
|
56
|
+
info message
|
57
|
+
end
|
58
|
+
|
59
|
+
def log_event_end(event, time)
|
60
|
+
info "Event #{event.encoded_name} Finished in #{time.to_f.to_d.to_s} seconds\n\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
def log_event(event, &block)
|
64
|
+
log_event_start(event) if log_event?(event)
|
65
|
+
start_time = Time.now
|
66
|
+
block.call
|
67
|
+
total_time = Time.now - start_time
|
68
|
+
log_event_end(event, total_time) if log_event?(event)
|
69
|
+
rescue Exception => ex
|
70
|
+
log_exception(ex)
|
71
|
+
raise
|
72
|
+
end
|
73
|
+
|
74
|
+
def log_event?(event)
|
75
|
+
if event.is_internal?
|
76
|
+
WebsocketRails.config.log_internal_events?
|
77
|
+
else
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def log_data?(event)
|
83
|
+
LOGGABLE_DATA.include?(event.data.class)
|
84
|
+
end
|
85
|
+
|
86
|
+
def log_exception(exception)
|
87
|
+
logger.error(wrap(:error, self, "#{exception.class.name}: #{exception.message}"))
|
88
|
+
exception.backtrace.each { |line| logger.error(wrap(:error, self, line)) } if exception.backtrace
|
89
|
+
logger << "\n"
|
90
|
+
rescue Exception => ex
|
91
|
+
puts '--- FATAL ---'
|
92
|
+
puts 'an exception occured while logging an exception'
|
93
|
+
puts ex.message, ex.backtrace
|
94
|
+
puts exception.message, exception.backtrace
|
95
|
+
end
|
96
|
+
|
97
|
+
def wrap(level, object, message, options = {})
|
98
|
+
header = options[:header] || object.log_header
|
99
|
+
color = color_for_level(level)
|
100
|
+
"[#{colorize(color, header)}] #{message.chomp}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def colorize(color, text)
|
104
|
+
"\e[#{ANSI[color]}m#{text}\e[0m"
|
105
|
+
end
|
106
|
+
|
107
|
+
def color_for_level(level)
|
108
|
+
case level
|
109
|
+
when :info then :green
|
110
|
+
when :debug then :yellow
|
111
|
+
else
|
112
|
+
:red
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def log_header
|
117
|
+
self.class.name.split('::').last
|
118
|
+
end
|
119
|
+
|
120
|
+
module Format
|
121
|
+
class << self
|
122
|
+
def format(severity, time, progname, msg)
|
123
|
+
"#{severity[0, 1]} [#{format_time(time)}] #{msg}\n"
|
124
|
+
end
|
125
|
+
|
126
|
+
def format_time(time)
|
127
|
+
time.strftime("%Y-%m-%d %H:%M:%S.") << time.usec.to_s[0, 3]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require "redis/connection/synchrony"
|
2
|
+
require "redis"
|
3
|
+
require "redis/connection/ruby"
|
4
|
+
|
5
|
+
module WebsocketRails
|
6
|
+
class Synchronization
|
7
|
+
|
8
|
+
def self.all_users
|
9
|
+
singleton.all_users
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.find_user(connection)
|
13
|
+
singleton.find_user connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.register_user(connection)
|
17
|
+
singleton.register_user connection
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.destroy_user(connection)
|
21
|
+
singleton.destroy_user connection
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.publish(event)
|
25
|
+
singleton.publish event
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.synchronize!
|
29
|
+
singleton.synchronize!
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.shutdown!
|
33
|
+
singleton.shutdown!
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.redis
|
37
|
+
singleton.redis
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.singleton
|
41
|
+
@singleton ||= new
|
42
|
+
end
|
43
|
+
|
44
|
+
include Logging
|
45
|
+
|
46
|
+
def redis
|
47
|
+
@redis ||= begin
|
48
|
+
redis_options = WebsocketRails.config.redis_options
|
49
|
+
EM.reactor_running? ? Redis.new(redis_options) : ruby_redis
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def ruby_redis
|
54
|
+
@ruby_redis ||= begin
|
55
|
+
redis_options = WebsocketRails.config.redis_options.merge(:driver => :ruby)
|
56
|
+
Redis.new(redis_options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def publish(event)
|
61
|
+
Fiber.new do
|
62
|
+
event.server_token = server_token
|
63
|
+
redis.publish "websocket_rails.events", event.serialize
|
64
|
+
end.resume
|
65
|
+
end
|
66
|
+
|
67
|
+
def server_token
|
68
|
+
@server_token
|
69
|
+
end
|
70
|
+
|
71
|
+
def synchronize!
|
72
|
+
unless @synchronizing
|
73
|
+
@server_token = generate_server_token
|
74
|
+
register_server(@server_token)
|
75
|
+
|
76
|
+
synchro = Fiber.new do
|
77
|
+
fiber_redis = Redis.connect(WebsocketRails.config.redis_options)
|
78
|
+
fiber_redis.subscribe "websocket_rails.events" do |on|
|
79
|
+
|
80
|
+
on.message do |_, encoded_event|
|
81
|
+
event = Event.new_from_json(encoded_event, nil)
|
82
|
+
|
83
|
+
# Do nothing if this is the server that sent this event.
|
84
|
+
next if event.server_token == server_token
|
85
|
+
|
86
|
+
# Ensure an event never gets triggered twice. Events added to the
|
87
|
+
# redis queue from other processes may not have a server token
|
88
|
+
# attached.
|
89
|
+
event.server_token = server_token if event.server_token.nil?
|
90
|
+
|
91
|
+
trigger_incoming event
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
info "Beginning Synchronization"
|
96
|
+
end
|
97
|
+
|
98
|
+
@synchronizing = true
|
99
|
+
|
100
|
+
EM.next_tick { synchro.resume }
|
101
|
+
|
102
|
+
trap('TERM') do
|
103
|
+
Thread.new { shutdown! }
|
104
|
+
end
|
105
|
+
trap('INT') do
|
106
|
+
Thread.new { shutdown! }
|
107
|
+
end
|
108
|
+
trap('QUIT') do
|
109
|
+
Thread.new { shutdown! }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def trigger_incoming(event)
|
115
|
+
case
|
116
|
+
when event.is_channel?
|
117
|
+
WebsocketRails[event.channel].trigger_event(event)
|
118
|
+
when event.is_user?
|
119
|
+
connection = WebsocketRails.users[event.user_id.to_s]
|
120
|
+
return if connection.nil?
|
121
|
+
connection.trigger event
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def shutdown!
|
126
|
+
remove_server(server_token)
|
127
|
+
end
|
128
|
+
|
129
|
+
def generate_server_token
|
130
|
+
begin
|
131
|
+
token = SecureRandom.urlsafe_base64
|
132
|
+
end while redis.sismember("websocket_rails.active_servers", token)
|
133
|
+
|
134
|
+
token
|
135
|
+
end
|
136
|
+
|
137
|
+
def register_server(token)
|
138
|
+
Fiber.new do
|
139
|
+
redis.sadd "websocket_rails.active_servers", token
|
140
|
+
info "Server Registered: #{token}"
|
141
|
+
end.resume
|
142
|
+
end
|
143
|
+
|
144
|
+
def remove_server(token)
|
145
|
+
ruby_redis.srem "websocket_rails.active_servers", token
|
146
|
+
info "Server Removed: #{token}"
|
147
|
+
EM.stop
|
148
|
+
end
|
149
|
+
|
150
|
+
def register_user(connection)
|
151
|
+
Fiber.new do
|
152
|
+
id = connection.user_identifier
|
153
|
+
user = connection.user
|
154
|
+
redis.hset 'websocket_rails.users', id, user.as_json(root: false).to_json
|
155
|
+
end.resume
|
156
|
+
end
|
157
|
+
|
158
|
+
def destroy_user(identifier)
|
159
|
+
Fiber.new do
|
160
|
+
redis.hdel 'websocket_rails.users', identifier
|
161
|
+
end.resume
|
162
|
+
end
|
163
|
+
|
164
|
+
def find_user(identifier)
|
165
|
+
Fiber.new do
|
166
|
+
raw_user = redis.hget('websocket_rails.users', identifier)
|
167
|
+
raw_user ? JSON.parse(raw_user) : nil
|
168
|
+
end.resume
|
169
|
+
end
|
170
|
+
|
171
|
+
def all_users
|
172
|
+
Fiber.new do
|
173
|
+
redis.hgetall('websocket_rails.users')
|
174
|
+
end.resume
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|