rage-rb 1.7.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -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/cli.rb +12 -5
- data/lib/rage/code_loader.rb +9 -0
- data/lib/rage/configuration.rb +67 -0
- data/lib/rage/controller/api.rb +8 -9
- data/lib/rage/cookies.rb +2 -2
- data/lib/rage/ext/active_record/connection_pool.rb +11 -3
- data/lib/rage/ext/setup.rb +1 -1
- data/lib/rage/middleware/fiber_wrapper.rb +3 -1
- data/lib/rage/middleware/origin_validator.rb +38 -0
- data/lib/rage/rails.rb +4 -1
- data/lib/rage/request.rb +39 -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: d8b2423a2cb780fd3f8ffc4e76a6690fff37162836e3d2d64791c7f058b3d63f
|
4
|
+
data.tar.gz: 86c2ee3464e5668e78409d7eae2e987c4e6a5aa54bea024c7cca694dd142019e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc810e46ff275e4595c2631f3cb5cbd19d38dd42885b393bb240ffb8b96ff6cb6f0f93015a461c469cbb88e13cbcc53288f385db590bd2562dc604a4d38b423e
|
7
|
+
data.tar.gz: 6be5256d0c92fd495f6afa2ba3d9f6a08cfe6058429040f10d035594ad9a54d050241fa443029836042e7435681563f36bfee89b04eecaeb31055206adff41ef
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.9.0] - 2024-08-24
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Static file server (#100).
|
8
|
+
- Rails 7.2 compatibility (#101).
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
- Correctly set Rails env (#102).
|
13
|
+
|
14
|
+
## [1.8.0] - 2024-08-06
|
15
|
+
|
16
|
+
### Added
|
17
|
+
|
18
|
+
- Support WebSockets (#88).
|
19
|
+
|
3
20
|
## [1.7.0] - 2024-07-30
|
4
21
|
|
5
22
|
### Added
|
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
|
-
![Requests per second
|
162
|
+
![Requests per second](https://github.com/user-attachments/assets/04678788-0034-4db4-9582-d0bc16fd9e28)
|
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
|