rage-rb 1.7.0 → 1.8.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/OVERVIEW.md +1 -1
- data/README.md +4 -2
- data/lib/rage/all.rb +1 -0
- data/lib/rage/cable/cable.rb +130 -0
- data/lib/rage/cable/channel.rb +452 -0
- data/lib/rage/cable/connection.rb +78 -0
- data/lib/rage/cable/protocol/actioncable_v1_json.rb +167 -0
- data/lib/rage/cable/router.rb +138 -0
- data/lib/rage/code_loader.rb +9 -0
- data/lib/rage/configuration.rb +53 -0
- data/lib/rage/controller/api.rb +8 -9
- data/lib/rage/cookies.rb +2 -2
- data/lib/rage/middleware/fiber_wrapper.rb +3 -1
- data/lib/rage/middleware/origin_validator.rb +38 -0
- data/lib/rage/router/dsl.rb +1 -1
- data/lib/rage/session.rb +2 -2
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +23 -15
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4adc4048bfa84558a6b86b15cbcab8e6153858878425bde66db78a975fa46f46
|
4
|
+
data.tar.gz: 46bef9f42fdd681bdf593f037a73ca8ae2d435acad9eed5ddf4b3740bf61b601
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 754b954d46d065020e0c2184c04f1a13d0c0a64ddf11d1b034062df4cc669460c259032a8bf40a61c876579f1774e50e9b57181d4d3e1301c4610577e11c6734
|
7
|
+
data.tar.gz: 4207556922f8fd2c82d07ba8664f53847dc1e5afb6bb5d38260ac0ccd428dd011fd8c6bf34f69aa75af97044a838df23c6c36b5baae1ae7cd3d4c08455fc699b
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/OVERVIEW.md
CHANGED
@@ -31,7 +31,7 @@ class UsersController < RageController::API
|
|
31
31
|
end
|
32
32
|
```
|
33
33
|
|
34
|
-
Before processing requests to `UsersController#show`, Rage has to [register](https://github.com/rage-rb/rage/blob/master/lib/rage/controller/api.rb#
|
34
|
+
Before processing requests to `UsersController#show`, Rage has to [register](https://github.com/rage-rb/rage/blob/master/lib/rage/controller/api.rb#L11) the show action. Registering means defining a new method that will look like this:
|
35
35
|
|
36
36
|
```ruby
|
37
37
|
class UsersController
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ Start coding!
|
|
44
44
|
|
45
45
|
## Getting Started
|
46
46
|
|
47
|
-
This gem is designed to be a drop-in replacement for Rails in API mode. Public API is
|
47
|
+
This gem is designed to be a drop-in replacement for Rails in API mode. Public API is expected to fully match Rails.
|
48
48
|
|
49
49
|
Check out in-depth API docs for more information:
|
50
50
|
|
@@ -60,6 +60,8 @@ Also, see the following integration guides:
|
|
60
60
|
- [Rails integration](https://github.com/rage-rb/rage/wiki/Rails-integration)
|
61
61
|
- [RSpec integration](https://github.com/rage-rb/rage/wiki/RSpec-integration)
|
62
62
|
|
63
|
+
If you are a first-time contributor, make sure to check the [overview doc](https://github.com/rage-rb/rage/blob/master/OVERVIEW.md) that shows how Rage's core components interact with each other.
|
64
|
+
|
63
65
|
### Example
|
64
66
|
|
65
67
|
A sample controller could look like this:
|
@@ -157,7 +159,7 @@ class BenchmarksController < ApplicationController
|
|
157
159
|
end
|
158
160
|
```
|
159
161
|
|
160
|
-

|
161
163
|
|
162
164
|
## Upcoming releases
|
163
165
|
|
data/lib/rage/all.rb
CHANGED
@@ -26,6 +26,7 @@ require_relative "logger/text_formatter"
|
|
26
26
|
require_relative "logger/json_formatter"
|
27
27
|
require_relative "logger/logger"
|
28
28
|
|
29
|
+
require_relative "middleware/origin_validator"
|
29
30
|
require_relative "middleware/fiber_wrapper"
|
30
31
|
require_relative "middleware/cors"
|
31
32
|
require_relative "middleware/reloader"
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rage::Cable
|
4
|
+
# Create a new Cable application.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# map "/cable" do
|
8
|
+
# run Rage.cable.application
|
9
|
+
# end
|
10
|
+
def self.application
|
11
|
+
protocol = Rage.config.cable.protocol
|
12
|
+
protocol.init(__router)
|
13
|
+
|
14
|
+
handler = __build_handler(protocol)
|
15
|
+
accept_response = [0, protocol.protocol_definition, []]
|
16
|
+
|
17
|
+
application = ->(env) do
|
18
|
+
if env["rack.upgrade?"] == :websocket
|
19
|
+
env["rack.upgrade"] = handler
|
20
|
+
accept_response
|
21
|
+
else
|
22
|
+
[426, { "Connection" => "Upgrade", "Upgrade" => "websocket" }, []]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Rage.with_middlewares(application, Rage.config.cable.middlewares)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
def self.__router
|
31
|
+
@__router ||= Router.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def self.__build_handler(protocol)
|
36
|
+
klass = Class.new do
|
37
|
+
def initialize(protocol)
|
38
|
+
Iodine.on_state(:on_start) do
|
39
|
+
unless Fiber.scheduler
|
40
|
+
Fiber.set_scheduler(Rage::FiberScheduler.new)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@protocol = protocol
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_open(connection)
|
48
|
+
Fiber.schedule do
|
49
|
+
@protocol.on_open(connection)
|
50
|
+
rescue => e
|
51
|
+
log_error(e)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_message(connection, data)
|
56
|
+
Fiber.schedule do
|
57
|
+
@protocol.on_message(connection, data)
|
58
|
+
rescue => e
|
59
|
+
log_error(e)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if protocol.respond_to?(:on_close)
|
64
|
+
def on_close(connection)
|
65
|
+
return unless ::Iodine.running?
|
66
|
+
|
67
|
+
Fiber.schedule do
|
68
|
+
@protocol.on_close(connection)
|
69
|
+
rescue => e
|
70
|
+
log_error(e)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if protocol.respond_to?(:on_shutdown)
|
76
|
+
def on_shutdown(connection)
|
77
|
+
@protocol.on_shutdown(connection)
|
78
|
+
rescue => e
|
79
|
+
log_error(e)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def log_error(e)
|
86
|
+
Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
klass.new(protocol)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Broadcast data directly to a named stream.
|
94
|
+
#
|
95
|
+
# @param stream [String] the name of the stream
|
96
|
+
# @param data [Object] the object to send to the clients. This will later be encoded according to the protocol used.
|
97
|
+
# @example
|
98
|
+
# Rage.cable.broadcast("chat", { message: "A new member has joined!" })
|
99
|
+
def self.broadcast(stream, data)
|
100
|
+
Rage.config.cable.protocol.broadcast(stream, data)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @!parse [ruby]
|
104
|
+
# # @abstract
|
105
|
+
# class WebSocketConnection
|
106
|
+
# # Write data to the connection.
|
107
|
+
# #
|
108
|
+
# # @param data [String] the data to write
|
109
|
+
# def write(data)
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# # Subscribe to a channel.
|
113
|
+
# #
|
114
|
+
# # @param name [String] the channel name
|
115
|
+
# def subscribe(name)
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# # Close the connection.
|
119
|
+
# def close
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
|
123
|
+
module Protocol
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
require_relative "protocol/actioncable_v1_json"
|
128
|
+
require_relative "channel"
|
129
|
+
require_relative "connection"
|
130
|
+
require_relative "router"
|
@@ -0,0 +1,452 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
class Rage::Cable::Channel
|
4
|
+
# @private
|
5
|
+
INTERNAL_ACTIONS = [:subscribed, :unsubscribed]
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# @private
|
9
|
+
attr_reader :__prepared_actions
|
10
|
+
|
11
|
+
# @private
|
12
|
+
attr_reader :__channels
|
13
|
+
|
14
|
+
# @private
|
15
|
+
# returns a list of actions that can be called remotely
|
16
|
+
def __register_actions
|
17
|
+
actions = (
|
18
|
+
public_instance_methods(true) - Rage::Cable::Channel.public_instance_methods(true)
|
19
|
+
).reject { |m| m.start_with?("__rage_tmp") || m.start_with?("__run") }
|
20
|
+
|
21
|
+
@__prepared_actions = (INTERNAL_ACTIONS + actions).each_with_object({}) do |action_name, memo|
|
22
|
+
memo[action_name] = __register_action_proc(action_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
actions - INTERNAL_ACTIONS
|
26
|
+
end
|
27
|
+
|
28
|
+
# @private
|
29
|
+
# rubocop:disable Layout/HeredocIndentation, Layout/IndentationWidth, Layout/EndAlignment, Layout/ElseAlignment
|
30
|
+
def __register_action_proc(action_name)
|
31
|
+
if action_name == :subscribed && @__hooks
|
32
|
+
before_subscribe_chunk = if @__hooks[:before_subscribe]
|
33
|
+
lines = @__hooks[:before_subscribe].map do |h|
|
34
|
+
condition = if h[:if] && h[:unless]
|
35
|
+
"if #{h[:if]} && !#{h[:unless]}"
|
36
|
+
elsif h[:if]
|
37
|
+
"if #{h[:if]}"
|
38
|
+
elsif h[:unless]
|
39
|
+
"unless #{h[:unless]}"
|
40
|
+
end
|
41
|
+
|
42
|
+
<<~RUBY
|
43
|
+
#{h[:name]} #{condition}
|
44
|
+
return if @__subscription_rejected
|
45
|
+
RUBY
|
46
|
+
end
|
47
|
+
|
48
|
+
lines.join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
after_subscribe_chunk = if @__hooks[:after_subscribe]
|
52
|
+
lines = @__hooks[:after_subscribe].map do |h|
|
53
|
+
condition = if h[:if] && h[:unless]
|
54
|
+
"if #{h[:if]} && !#{h[:unless]}"
|
55
|
+
elsif h[:if]
|
56
|
+
"if #{h[:if]}"
|
57
|
+
elsif h[:unless]
|
58
|
+
"unless #{h[:unless]}"
|
59
|
+
end
|
60
|
+
|
61
|
+
<<~RUBY
|
62
|
+
#{h[:name]} #{condition}
|
63
|
+
RUBY
|
64
|
+
end
|
65
|
+
|
66
|
+
lines.join("\n")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if action_name == :unsubscribed && @__hooks
|
71
|
+
before_unsubscribe_chunk = if @__hooks[:before_unsubscribe]
|
72
|
+
lines = @__hooks[:before_unsubscribe].map do |h|
|
73
|
+
condition = if h[:if] && h[:unless]
|
74
|
+
"if #{h[:if]} && !#{h[:unless]}"
|
75
|
+
elsif h[:if]
|
76
|
+
"if #{h[:if]}"
|
77
|
+
elsif h[:unless]
|
78
|
+
"unless #{h[:unless]}"
|
79
|
+
end
|
80
|
+
|
81
|
+
<<~RUBY
|
82
|
+
#{h[:name]} #{condition}
|
83
|
+
RUBY
|
84
|
+
end
|
85
|
+
|
86
|
+
lines.join("\n")
|
87
|
+
end
|
88
|
+
|
89
|
+
after_unsubscribe_chunk = if @__hooks[:after_unsubscribe]
|
90
|
+
lines = @__hooks[:after_unsubscribe].map do |h|
|
91
|
+
condition = if h[:if] && h[:unless]
|
92
|
+
"if #{h[:if]} && !#{h[:unless]}"
|
93
|
+
elsif h[:if]
|
94
|
+
"if #{h[:if]}"
|
95
|
+
elsif h[:unless]
|
96
|
+
"unless #{h[:unless]}"
|
97
|
+
end
|
98
|
+
|
99
|
+
<<~RUBY
|
100
|
+
#{h[:name]} #{condition}
|
101
|
+
RUBY
|
102
|
+
end
|
103
|
+
|
104
|
+
lines.join("\n")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
rescue_handlers_chunk = if @__rescue_handlers
|
109
|
+
lines = @__rescue_handlers.map do |klasses, handler|
|
110
|
+
<<~RUBY
|
111
|
+
rescue #{klasses.join(", ")} => __e
|
112
|
+
#{instance_method(handler).arity == 0 ? handler : "#{handler}(__e)"}
|
113
|
+
RUBY
|
114
|
+
end
|
115
|
+
|
116
|
+
lines.join("\n")
|
117
|
+
else
|
118
|
+
""
|
119
|
+
end
|
120
|
+
|
121
|
+
periodic_timers_chunk = if @__periodic_timers
|
122
|
+
set_up_periodic_timers
|
123
|
+
|
124
|
+
if action_name == :subscribed
|
125
|
+
<<~RUBY
|
126
|
+
self.class.__channels << self unless subscription_rejected?
|
127
|
+
RUBY
|
128
|
+
elsif action_name == :unsubscribed
|
129
|
+
<<~RUBY
|
130
|
+
self.class.__channels.delete(self)
|
131
|
+
RUBY
|
132
|
+
end
|
133
|
+
else
|
134
|
+
""
|
135
|
+
end
|
136
|
+
|
137
|
+
is_subscribing = action_name == :subscribed
|
138
|
+
activerecord_loaded = defined?(::ActiveRecord)
|
139
|
+
|
140
|
+
method_name = class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
141
|
+
def __run_#{action_name}(data)
|
142
|
+
#{if is_subscribing
|
143
|
+
<<~RUBY
|
144
|
+
@__is_subscribing = true
|
145
|
+
RUBY
|
146
|
+
end}
|
147
|
+
|
148
|
+
#{before_subscribe_chunk}
|
149
|
+
#{before_unsubscribe_chunk}
|
150
|
+
|
151
|
+
#{if instance_method(action_name).arity == 0
|
152
|
+
<<~RUBY
|
153
|
+
#{action_name}
|
154
|
+
RUBY
|
155
|
+
else
|
156
|
+
<<~RUBY
|
157
|
+
#{action_name}(data)
|
158
|
+
RUBY
|
159
|
+
end}
|
160
|
+
|
161
|
+
#{after_subscribe_chunk}
|
162
|
+
#{after_unsubscribe_chunk}
|
163
|
+
#{periodic_timers_chunk}
|
164
|
+
#{rescue_handlers_chunk}
|
165
|
+
|
166
|
+
#{if activerecord_loaded
|
167
|
+
<<~RUBY
|
168
|
+
ensure
|
169
|
+
if ActiveRecord::Base.connection_pool.active_connection?
|
170
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
171
|
+
end
|
172
|
+
RUBY
|
173
|
+
end}
|
174
|
+
end
|
175
|
+
RUBY
|
176
|
+
|
177
|
+
eval("->(channel, data) { channel.#{method_name}(data) }")
|
178
|
+
end
|
179
|
+
# rubocop:enable all
|
180
|
+
|
181
|
+
# @private
|
182
|
+
def __prepare_id_method(method_name)
|
183
|
+
define_method(method_name) do
|
184
|
+
@__identified_by[method_name]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Register a new `before_subscribe` hook that will be called before the {subscribed} method.
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# before_subscribe :my_method
|
192
|
+
# @example
|
193
|
+
# before_subscribe do
|
194
|
+
# ...
|
195
|
+
# end
|
196
|
+
# @example
|
197
|
+
# before_subscribe :my_method, if: -> { ... }
|
198
|
+
def before_subscribe(action_name = nil, **opts, &block)
|
199
|
+
add_action(:before_subscribe, action_name, **opts, &block)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Register a new `after_subscribe` hook that will be called after the {subscribed} method.
|
203
|
+
#
|
204
|
+
# @example
|
205
|
+
# after_subscribe do
|
206
|
+
# ...
|
207
|
+
# end
|
208
|
+
# @example
|
209
|
+
# after_subscribe :my_method, unless: :subscription_rejected?
|
210
|
+
# @note This callback will be triggered even if the subscription was rejected with the {reject} method.
|
211
|
+
def after_subscribe(action_name = nil, **opts, &block)
|
212
|
+
add_action(:after_subscribe, action_name, **opts, &block)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Register a new `before_unsubscribe` hook that will be called before the {unsubscribed} method.
|
216
|
+
def before_unsubscribe(action_name = nil, **opts, &block)
|
217
|
+
add_action(:before_unsubscribe, action_name, **opts, &block)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Register a new `after_unsubscribe` hook that will be called after the {unsubscribed} method.
|
221
|
+
def after_unsubscribe(action_name = nil, **opts, &block)
|
222
|
+
add_action(:after_unsubscribe, action_name, **opts, &block)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Register an exception handler.
|
226
|
+
#
|
227
|
+
# @param klasses [Class, Array<Class>] exception classes to watch on
|
228
|
+
# @param with [Symbol] the name of a handler method. The method can take one argument, which is the raised exception. Alternatively, you can pass a block, which can also take one argument.
|
229
|
+
# @example
|
230
|
+
# rescue_from StandardError, with: :report_error
|
231
|
+
#
|
232
|
+
# private
|
233
|
+
#
|
234
|
+
# def report_error(e)
|
235
|
+
# SomeExternalBugtrackingService.notify(e)
|
236
|
+
# end
|
237
|
+
# @example
|
238
|
+
# rescue_from StandardError do |e|
|
239
|
+
# SomeExternalBugtrackingService.notify(e)
|
240
|
+
# end
|
241
|
+
def rescue_from(*klasses, with: nil, &block)
|
242
|
+
unless with
|
243
|
+
if block_given?
|
244
|
+
with = define_tmp_method(block)
|
245
|
+
else
|
246
|
+
raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
if @__rescue_handlers.nil?
|
251
|
+
@__rescue_handlers = []
|
252
|
+
elsif @__rescue_handlers.frozen?
|
253
|
+
@__rescue_handlers = @__rescue_handlers.dup
|
254
|
+
end
|
255
|
+
|
256
|
+
@__rescue_handlers.unshift([klasses, with])
|
257
|
+
end
|
258
|
+
|
259
|
+
# Set up a timer to periodically perform a task on the channel. Accepts a method name or a block.
|
260
|
+
#
|
261
|
+
# @param method_name [Symbol, nil] the name of the method to call
|
262
|
+
# @param every [Integer] the calling period in seconds
|
263
|
+
# @example
|
264
|
+
# periodically every: 3.minutes do
|
265
|
+
# transmit({ action: :update_count, count: current_count })
|
266
|
+
# end
|
267
|
+
# @example
|
268
|
+
# periodically :update_count, every: 3.minutes
|
269
|
+
def periodically(method_name = nil, every:, &block)
|
270
|
+
callback_name = if block_given?
|
271
|
+
raise ArgumentError, "Pass the `method_name` argument or provide a block, not both" if method_name
|
272
|
+
define_tmp_method(block)
|
273
|
+
elsif method_name.is_a?(Symbol)
|
274
|
+
define_tmp_method(eval("-> { #{method_name} }"))
|
275
|
+
else
|
276
|
+
raise ArgumentError, "Expected a Symbol method name, got #{method_name.inspect}"
|
277
|
+
end
|
278
|
+
|
279
|
+
unless every.is_a?(Numeric) && every > 0
|
280
|
+
raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
|
281
|
+
end
|
282
|
+
|
283
|
+
callback = eval("->(channel) { channel.#{callback_name} }")
|
284
|
+
|
285
|
+
if @__periodic_timers.nil?
|
286
|
+
@__periodic_timers = []
|
287
|
+
elsif @__periodic_timers.frozen?
|
288
|
+
@__periodic_timers = @__periodic_timers.dup
|
289
|
+
end
|
290
|
+
|
291
|
+
@__periodic_timers << [callback, every]
|
292
|
+
end
|
293
|
+
|
294
|
+
protected
|
295
|
+
|
296
|
+
def set_up_periodic_timers
|
297
|
+
return if @__periodic_timers_set_up
|
298
|
+
|
299
|
+
@__channels = Set.new
|
300
|
+
|
301
|
+
@__periodic_timers.each do |callback, every|
|
302
|
+
::Iodine.run_every((every * 1000).to_i) do
|
303
|
+
slice_length = (@__channels.length / 20.0).ceil
|
304
|
+
|
305
|
+
if slice_length != 0
|
306
|
+
@__channels.each_slice(slice_length) do |slice|
|
307
|
+
Fiber.schedule do
|
308
|
+
slice.each { |channel| callback.call(channel) }
|
309
|
+
rescue => e
|
310
|
+
Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
@__periodic_timers_set_up = true
|
318
|
+
end
|
319
|
+
|
320
|
+
def add_action(action_type, action_name = nil, **opts, &block)
|
321
|
+
if block_given?
|
322
|
+
action_name = define_tmp_method(block)
|
323
|
+
elsif action_name.nil?
|
324
|
+
raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
|
325
|
+
end
|
326
|
+
|
327
|
+
_if, _unless = opts.values_at(:if, :unless)
|
328
|
+
|
329
|
+
action = {
|
330
|
+
name: action_name,
|
331
|
+
if: _if,
|
332
|
+
unless: _unless
|
333
|
+
}
|
334
|
+
|
335
|
+
action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
|
336
|
+
action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
|
337
|
+
|
338
|
+
if @__hooks.nil?
|
339
|
+
@__hooks = {}
|
340
|
+
elsif @__hooks[action_type] && @__hooks.frozen?
|
341
|
+
@__hooks = @__hooks.dup
|
342
|
+
@__hooks[action_type] = @__hooks[action_type].dup
|
343
|
+
end
|
344
|
+
|
345
|
+
if @__hooks[action_type].nil?
|
346
|
+
@__hooks[action_type] = [action]
|
347
|
+
elsif (i = @__hooks[action_type].find_index { |a| a[:name] == action_name })
|
348
|
+
@__hooks[action_type][i] = action
|
349
|
+
else
|
350
|
+
@__hooks[action_type] << action
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
attr_writer :__hooks, :__rescue_handlers, :__periodic_timers
|
355
|
+
|
356
|
+
def inherited(klass)
|
357
|
+
klass.__hooks = @__hooks.freeze
|
358
|
+
klass.__rescue_handlers = @__rescue_handlers.freeze
|
359
|
+
klass.__periodic_timers = @__periodic_timers.freeze
|
360
|
+
end
|
361
|
+
|
362
|
+
@@__tmp_name_seed = ("a".."i").to_a.permutation
|
363
|
+
|
364
|
+
def define_tmp_method(block)
|
365
|
+
name = @@__tmp_name_seed.next.join
|
366
|
+
define_method("__rage_tmp_#{name}", block)
|
367
|
+
end
|
368
|
+
end # class << self
|
369
|
+
|
370
|
+
# @private
|
371
|
+
def __has_action?(action_name)
|
372
|
+
!INTERNAL_ACTIONS.include?(action_name) && self.class.__prepared_actions.has_key?(action_name)
|
373
|
+
end
|
374
|
+
|
375
|
+
# @private
|
376
|
+
def __run_action(action_name, data = nil)
|
377
|
+
self.class.__prepared_actions[action_name].call(self, data)
|
378
|
+
end
|
379
|
+
|
380
|
+
# @private
|
381
|
+
def initialize(connection, params, identified_by)
|
382
|
+
@__connection = connection
|
383
|
+
@__params = params
|
384
|
+
@__identified_by = identified_by
|
385
|
+
end
|
386
|
+
|
387
|
+
# Get the params hash passed in during the subscription process.
|
388
|
+
#
|
389
|
+
# @return [Hash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass}]
|
390
|
+
def params
|
391
|
+
@__params
|
392
|
+
end
|
393
|
+
|
394
|
+
# Reject the subscription request. The method should only be called during the subscription
|
395
|
+
# process (i.e. inside the {subscribed} method or {before_subscribe}/{after_subscribe} hooks).
|
396
|
+
def reject
|
397
|
+
@__subscription_rejected = true
|
398
|
+
end
|
399
|
+
|
400
|
+
# Checks whether the {reject} method has been called.
|
401
|
+
#
|
402
|
+
# @return [Boolean]
|
403
|
+
def subscription_rejected?
|
404
|
+
!!@__subscription_rejected
|
405
|
+
end
|
406
|
+
|
407
|
+
# Subscribe to a stream.
|
408
|
+
#
|
409
|
+
# @param stream [String] the name of the stream
|
410
|
+
def stream_from(stream)
|
411
|
+
Rage.config.cable.protocol.subscribe(@__connection, stream, @__params)
|
412
|
+
end
|
413
|
+
|
414
|
+
# Broadcast data to all the clients subscribed to a stream.
|
415
|
+
#
|
416
|
+
# @param stream [String] the name of the stream
|
417
|
+
# @param data [Object] the data to send to the clients
|
418
|
+
# @example
|
419
|
+
# def subscribed
|
420
|
+
# broadcast("notifications", { message: "A new member has joined!" })
|
421
|
+
# end
|
422
|
+
def broadcast(stream, data)
|
423
|
+
Rage.config.cable.protocol.broadcast(stream, data)
|
424
|
+
end
|
425
|
+
|
426
|
+
# Transmit data to the current client.
|
427
|
+
#
|
428
|
+
# @param data [Object] the data to send to the client
|
429
|
+
# @example
|
430
|
+
# def subscribed
|
431
|
+
# transmit({ message: "Hello!" })
|
432
|
+
# end
|
433
|
+
def transmit(data)
|
434
|
+
message = Rage.config.cable.protocol.serialize(@__params, data)
|
435
|
+
|
436
|
+
if @__is_subscribing
|
437
|
+
# we expect a confirmation message to be sent as a result of a successful subscribe call;
|
438
|
+
# this will make sure `transmit` calls send data after the confirmation;
|
439
|
+
::Iodine.defer { @__connection.write(message) }
|
440
|
+
else
|
441
|
+
@__connection.write(message)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# Called once a client has become a subscriber of the channel.
|
446
|
+
def subscribed
|
447
|
+
end
|
448
|
+
|
449
|
+
# Called once a client unsubscribes from the channel.
|
450
|
+
def unsubscribed
|
451
|
+
end
|
452
|
+
end
|