rage-rb 1.10.1 → 1.12.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 +22 -0
- data/Gemfile +2 -0
- data/README.md +6 -2
- data/lib/rage/cable/adapters/base.rb +16 -0
- data/lib/rage/cable/adapters/redis.rb +127 -0
- data/lib/rage/cable/cable.rb +23 -5
- data/lib/rage/cable/channel.rb +1 -1
- data/lib/rage/cable/protocol/actioncable_v1_json.rb +32 -7
- data/lib/rage/code_loader.rb +8 -0
- data/lib/rage/configuration.rb +50 -0
- data/lib/rage/controller/api.rb +120 -29
- data/lib/rage/cookies.rb +1 -1
- data/lib/rage/ext/setup.rb +6 -4
- data/lib/rage/openapi/builder.rb +85 -0
- data/lib/rage/openapi/collector.rb +44 -0
- data/lib/rage/openapi/converter.rb +141 -0
- data/lib/rage/openapi/index.html.erb +22 -0
- data/lib/rage/openapi/nodes/method.rb +27 -0
- data/lib/rage/openapi/nodes/parent.rb +16 -0
- data/lib/rage/openapi/nodes/root.rb +56 -0
- data/lib/rage/openapi/openapi.rb +146 -0
- data/lib/rage/openapi/parser.rb +204 -0
- data/lib/rage/openapi/parsers/ext/active_record.rb +62 -0
- data/lib/rage/openapi/parsers/ext/alba.rb +285 -0
- data/lib/rage/openapi/parsers/request.rb +18 -0
- data/lib/rage/openapi/parsers/response.rb +19 -0
- data/lib/rage/openapi/parsers/shared_reference.rb +25 -0
- data/lib/rage/openapi/parsers/yaml.rb +66 -0
- data/lib/rage/router/backend.rb +1 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +5 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c42516bec6fabfb0b06d9f11a5156298474a48f7988edba746122ae4ec172dc
|
4
|
+
data.tar.gz: 3b6b5b7692bb693ba53137d11bc1d7883ace69e496b5a99ec367da9a78ec4675
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b72d88f12c3608006637d822c7d3f955ef549e61ee644fc3d7748f507381cfe4917d3eea7f6cb75cf49f053f7ba34ce407a8ed06033a06e89e95208a15b6144e
|
7
|
+
data.tar.gz: f60d1950bce78665acfe6af71aeff1694692b6063cbf76e6c39a0af5dbcf8a79672b8603c0a39ce5d2b0beb0ecf61edfba310d6140fb94b899d85dbad0dd2c21
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.12.0] - 2025-01-21
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Add Redis adapter (#114).
|
8
|
+
- Add global response tags (#110).
|
9
|
+
- Implement around_action callbacks (#107).
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Support date types in Alba serializers (#112).
|
14
|
+
|
15
|
+
## [1.11.0] - 2024-12-18
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- `Rage::OpenAPI` (#109).
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
|
23
|
+
- Correctly handle ActiveRecord connections in the environments with `legacy_connection_handling == false` (#108).
|
24
|
+
|
3
25
|
## [1.10.1] - 2024-09-17
|
4
26
|
|
5
27
|
### Fixed
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -6,8 +6,7 @@
|
|
6
6
|

|
7
7
|

|
8
8
|
|
9
|
-
|
10
|
-
Inspired by [Deno](https://deno.com) and built on top of [Iodine](https://github.com/rage-rb/iodine), this is a Ruby web framework that is based on the following design principles:
|
9
|
+
Rage is a high-performance framework compatible with Rails, featuring [WebSocket](https://github.com/rage-rb/rage/wiki/WebSockets-guide) support and automatic generation of [OpenAPI](https://github.com/rage-rb/rage/wiki/OpenAPI-Guide) documentation for your APIs. The framework is built on top of [Iodine](https://github.com/rage-rb/iodine) and is based on the following design principles:
|
11
10
|
|
12
11
|
* **Rails compatible API** - Rails' API is clean, straightforward, and simply makes sense. It was one of the reasons why Rails was so successful in the past.
|
13
12
|
|
@@ -46,6 +45,11 @@ Start coding!
|
|
46
45
|
|
47
46
|
This gem is designed to be a drop-in replacement for Rails in API mode. Public API is expected to fully match Rails.
|
48
47
|
|
48
|
+
A Rage application can operate in two modes:
|
49
|
+
|
50
|
+
* **Rails Mode**: Integrate Rage into an existing Rails application to improve throughput and better handle traffic spikes. For more information, see [Rails Integration](https://github.com/rage-rb/rage/wiki/Rails-integration).
|
51
|
+
* **Standalone Mode**: Build high-performance services with minimal setup using Rage. To get started, run `rage new --help` for more details.
|
52
|
+
|
49
53
|
Check out in-depth API docs for more information:
|
50
54
|
|
51
55
|
- [Controller API](https://rage-rb.pages.dev/RageController/API)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Cable::Adapters::Base
|
4
|
+
def pick_a_worker(&block)
|
5
|
+
_lock, lock_path = Tempfile.new.yield_self { |file| [file, file.path] }
|
6
|
+
|
7
|
+
Iodine.on_state(:on_start) do
|
8
|
+
if File.new(lock_path).flock(File::LOCK_EX | File::LOCK_NB)
|
9
|
+
if Rage.logger.debug?
|
10
|
+
puts "INFO: #{Process.pid} is managing #{self.class.name.split("::").last} subscriptions."
|
11
|
+
end
|
12
|
+
block.call
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
if !defined?(RedisClient)
|
6
|
+
fail <<~ERR
|
7
|
+
|
8
|
+
Redis adapter depends on the `redis-client` gem. Add the following line to your Gemfile:
|
9
|
+
gem "redis-client"
|
10
|
+
|
11
|
+
ERR
|
12
|
+
end
|
13
|
+
|
14
|
+
class Rage::Cable::Adapters::Redis < Rage::Cable::Adapters::Base
|
15
|
+
REDIS_STREAM_NAME = "rage:cable:messages"
|
16
|
+
DEFAULT_REDIS_OPTIONS = { reconnect_attempts: [0.05, 0.1, 0.5] }
|
17
|
+
|
18
|
+
def initialize(config)
|
19
|
+
@redis_stream = if (prefix = config.delete(:channel_prefix))
|
20
|
+
"#{prefix}:#{REDIS_STREAM_NAME}"
|
21
|
+
else
|
22
|
+
REDIS_STREAM_NAME
|
23
|
+
end
|
24
|
+
|
25
|
+
@redis_config = RedisClient.config(**DEFAULT_REDIS_OPTIONS.merge(config))
|
26
|
+
@server_uuid = SecureRandom.uuid
|
27
|
+
|
28
|
+
redis_version = get_redis_version
|
29
|
+
if redis_version < Gem::Version.create(5)
|
30
|
+
raise "Redis adapter only supports Redis 5+. Detected Redis version: #{redis_version}."
|
31
|
+
end
|
32
|
+
|
33
|
+
@trimming_strategy = redis_version < Gem::Version.create("6.2.0") ? :maxlen : :minid
|
34
|
+
|
35
|
+
pick_a_worker { poll }
|
36
|
+
end
|
37
|
+
|
38
|
+
def publish(stream_name, data)
|
39
|
+
message_uuid = SecureRandom.uuid
|
40
|
+
|
41
|
+
publish_redis.call(
|
42
|
+
"XADD",
|
43
|
+
@redis_stream,
|
44
|
+
trimming_method, "~", trimming_value,
|
45
|
+
"*",
|
46
|
+
"1", stream_name,
|
47
|
+
"2", data.to_json,
|
48
|
+
"3", @server_uuid,
|
49
|
+
"4", message_uuid
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def publish_redis
|
56
|
+
@publish_redis ||= @redis_config.new_client
|
57
|
+
end
|
58
|
+
|
59
|
+
def trimming_method
|
60
|
+
@trimming_strategy == :maxlen ? "MAXLEN" : "MINID"
|
61
|
+
end
|
62
|
+
|
63
|
+
def trimming_value
|
64
|
+
@trimming_strategy == :maxlen ? "10000" : ((Time.now.to_f - 5 * 60) * 1000).to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_redis_version
|
68
|
+
service_redis = @redis_config.new_client
|
69
|
+
version = service_redis.call("INFO").match(/redis_version:([[:graph:]]+)/)[1]
|
70
|
+
|
71
|
+
Gem::Version.create(version)
|
72
|
+
|
73
|
+
rescue RedisClient::Error => e
|
74
|
+
puts "FATAL: Couldn't connect to Redis - all broadcasts will be limited to the current server."
|
75
|
+
puts e.backtrace.join("\n")
|
76
|
+
Gem::Version.create(5)
|
77
|
+
|
78
|
+
ensure
|
79
|
+
service_redis.close
|
80
|
+
end
|
81
|
+
|
82
|
+
def error_backoff_intervals
|
83
|
+
@error_backoff_intervals ||= Enumerator.new do |y|
|
84
|
+
y << 0.2 << 0.5 << 1 << 2 << 5
|
85
|
+
loop { y << 10 }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def poll
|
90
|
+
unless Fiber.scheduler
|
91
|
+
Fiber.set_scheduler(Rage::FiberScheduler.new)
|
92
|
+
end
|
93
|
+
|
94
|
+
Iodine.on_state(:start_shutdown) do
|
95
|
+
@stopping = true
|
96
|
+
end
|
97
|
+
|
98
|
+
Fiber.schedule do
|
99
|
+
read_redis = @redis_config.new_client
|
100
|
+
last_id = (Time.now.to_f * 1000).to_i
|
101
|
+
last_message_uuid = nil
|
102
|
+
|
103
|
+
loop do
|
104
|
+
data = read_redis.blocking_call(5, "XREAD", "COUNT", "100", "BLOCK", "5000", "STREAMS", @redis_stream, last_id)
|
105
|
+
|
106
|
+
if data
|
107
|
+
data[@redis_stream].each do |id, (_, stream_name, _, serialized_data, _, broadcaster_uuid, _, message_uuid)|
|
108
|
+
if broadcaster_uuid != @server_uuid && message_uuid != last_message_uuid
|
109
|
+
Rage.config.cable.protocol.broadcast(stream_name, JSON.parse(serialized_data))
|
110
|
+
end
|
111
|
+
|
112
|
+
last_id = id
|
113
|
+
last_message_uuid = message_uuid
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
rescue RedisClient::Error => e
|
118
|
+
Rage.logger.error("Subscriber error: #{e.message} (#{e.class})")
|
119
|
+
sleep error_backoff_intervals.next
|
120
|
+
rescue => e
|
121
|
+
@stopping ? break : raise(e)
|
122
|
+
else
|
123
|
+
error_backoff_intervals.rewind
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/rage/cable/cable.rb
CHANGED
@@ -8,11 +8,11 @@ module Rage::Cable
|
|
8
8
|
# run Rage.cable.application
|
9
9
|
# end
|
10
10
|
def self.application
|
11
|
-
|
12
|
-
|
11
|
+
# explicitly initialize the adapter
|
12
|
+
__adapter
|
13
13
|
|
14
|
-
handler = __build_handler(
|
15
|
-
accept_response = [0,
|
14
|
+
handler = __build_handler(__protocol)
|
15
|
+
accept_response = [0, __protocol.protocol_definition, []]
|
16
16
|
|
17
17
|
application = ->(env) do
|
18
18
|
if env["rack.upgrade?"] == :websocket
|
@@ -31,6 +31,15 @@ module Rage::Cable
|
|
31
31
|
@__router ||= Router.new
|
32
32
|
end
|
33
33
|
|
34
|
+
# @private
|
35
|
+
def self.__protocol
|
36
|
+
@__protocol ||= Rage.config.cable.protocol.tap { |protocol| protocol.init(__router) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.__adapter
|
40
|
+
@__adapter ||= Rage.config.cable.adapter
|
41
|
+
end
|
42
|
+
|
34
43
|
# @private
|
35
44
|
def self.__build_handler(protocol)
|
36
45
|
klass = Class.new do
|
@@ -94,10 +103,14 @@ module Rage::Cable
|
|
94
103
|
#
|
95
104
|
# @param stream [String] the name of the stream
|
96
105
|
# @param data [Object] the object to send to the clients. This will later be encoded according to the protocol used.
|
106
|
+
# @return [true]
|
97
107
|
# @example
|
98
108
|
# Rage.cable.broadcast("chat", { message: "A new member has joined!" })
|
99
109
|
def self.broadcast(stream, data)
|
100
|
-
|
110
|
+
__protocol.broadcast(stream, data)
|
111
|
+
__adapter&.publish(stream, data)
|
112
|
+
|
113
|
+
true
|
101
114
|
end
|
102
115
|
|
103
116
|
# @!parse [ruby]
|
@@ -120,6 +133,11 @@ module Rage::Cable
|
|
120
133
|
# end
|
121
134
|
# end
|
122
135
|
|
136
|
+
module Adapters
|
137
|
+
autoload :Base, "rage/cable/adapters/base"
|
138
|
+
autoload :Redis, "rage/cable/adapters/redis"
|
139
|
+
end
|
140
|
+
|
123
141
|
module Protocol
|
124
142
|
end
|
125
143
|
end
|
data/lib/rage/cable/channel.rb
CHANGED
@@ -418,7 +418,7 @@ class Rage::Cable::Channel
|
|
418
418
|
# broadcast("notifications", { message: "A new member has joined!" })
|
419
419
|
# end
|
420
420
|
def broadcast(stream, data)
|
421
|
-
Rage.
|
421
|
+
Rage.cable.broadcast(stream, data)
|
422
422
|
end
|
423
423
|
|
424
424
|
# Transmit data to the current client.
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "zlib"
|
4
|
+
|
3
5
|
##
|
4
6
|
# A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
|
5
7
|
# The class that defines a protocol should respond to the following methods:
|
@@ -17,6 +19,9 @@
|
|
17
19
|
# * `on_shutdown`
|
18
20
|
# * `on_close`
|
19
21
|
#
|
22
|
+
# It is likely that all logic around `@subscription_identifiers` has nothing to do with the protocol itself and
|
23
|
+
# should be extracted into another class. We'll refactor this once we start working on a new protocol.
|
24
|
+
#
|
20
25
|
class Rage::Cable::Protocol::ActioncableV1Json
|
21
26
|
module TYPE
|
22
27
|
WELCOME = "welcome"
|
@@ -55,14 +60,30 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
55
60
|
def self.init(router)
|
56
61
|
@router = router
|
57
62
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
63
|
+
Iodine.on_state(:on_start) do
|
64
|
+
ping_counter = Time.now.to_i
|
65
|
+
|
66
|
+
Iodine.run_every(3000) do
|
67
|
+
ping_counter += 1
|
68
|
+
Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json, Iodine::PubSub::PROCESS)
|
69
|
+
end
|
62
70
|
end
|
63
71
|
|
64
72
|
# Hash<String(stream name) => Array<Hash>(subscription params)>
|
65
73
|
@subscription_identifiers = Hash.new { |hash, key| hash[key] = [] }
|
74
|
+
|
75
|
+
# this is a fallback to synchronize subscription identifiers across different worker processes;
|
76
|
+
# we expect connections to be distributed among all workers, so this code will almost never be called;
|
77
|
+
# we also synchronize subscriptions with the master process so that the forks that are spun up instead
|
78
|
+
# of the crashed ones also had access to the identifiers;
|
79
|
+
Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
|
80
|
+
stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
|
81
|
+
@subscription_identifiers[stream_name] << params unless @subscription_identifiers[stream_name].include?(params)
|
82
|
+
end
|
83
|
+
|
84
|
+
Iodine.on_state(:on_finish) do
|
85
|
+
Iodine.unsubscribe("cable:synchronize")
|
86
|
+
end
|
66
87
|
end
|
67
88
|
|
68
89
|
# The method is called any time a new WebSocket connection is established.
|
@@ -147,8 +168,12 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
147
168
|
# @param name [String] the stream name
|
148
169
|
# @param params [Hash] parameters associated with the client
|
149
170
|
def self.subscribe(connection, name, params)
|
150
|
-
connection.subscribe("cable:#{name}:#{params.
|
151
|
-
|
171
|
+
connection.subscribe("cable:#{name}:#{Zlib.crc32(params.to_s)}")
|
172
|
+
|
173
|
+
unless @subscription_identifiers[name].include?(params)
|
174
|
+
@subscription_identifiers[name] << params
|
175
|
+
::Iodine.publish("cable:synchronize", [name, params].to_json)
|
176
|
+
end
|
152
177
|
end
|
153
178
|
|
154
179
|
# Broadcast data to all clients connected to a stream.
|
@@ -160,7 +185,7 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
160
185
|
|
161
186
|
while i < identifiers.length
|
162
187
|
params = identifiers[i]
|
163
|
-
::Iodine.publish("cable:#{name}:#{params.
|
188
|
+
::Iodine.publish("cable:#{name}:#{Zlib.crc32(params.to_s)}", serialize(params, data))
|
164
189
|
i += 1
|
165
190
|
end
|
166
191
|
end
|
data/lib/rage/code_loader.rb
CHANGED
@@ -37,6 +37,10 @@ class Rage::CodeLoader
|
|
37
37
|
unless Rage.autoload?(:Cable) # the `Cable` component is loaded
|
38
38
|
Rage::Cable.__router.reset
|
39
39
|
end
|
40
|
+
|
41
|
+
unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
|
42
|
+
Rage::OpenAPI.__reset_data_cache
|
43
|
+
end
|
40
44
|
end
|
41
45
|
|
42
46
|
# in Rails mode - reset the routes; everything else will be done by Rails
|
@@ -49,6 +53,10 @@ class Rage::CodeLoader
|
|
49
53
|
unless Rage.autoload?(:Cable) # the `Cable` component is loaded
|
50
54
|
Rage::Cable.__router.reset
|
51
55
|
end
|
56
|
+
|
57
|
+
unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
|
58
|
+
Rage::OpenAPI.__reset_data_cache
|
59
|
+
end
|
52
60
|
end
|
53
61
|
|
54
62
|
def reloading?
|
data/lib/rage/configuration.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "yaml"
|
4
|
+
require "erb"
|
5
|
+
|
3
6
|
##
|
4
7
|
# `Rage.configure` can be used to adjust the behavior of your Rage application:
|
5
8
|
#
|
@@ -122,6 +125,17 @@
|
|
122
125
|
#
|
123
126
|
# > Allows requests from any origin.
|
124
127
|
#
|
128
|
+
# # OpenAPI Configuration
|
129
|
+
# • _config.openapi.tag_resolver_
|
130
|
+
#
|
131
|
+
# > Specifies the proc to build tags for API operations. The proc accepts the controller class, the symbol name of the action, and the default tag built by Rage.
|
132
|
+
#
|
133
|
+
# > ```ruby
|
134
|
+
# config.openapi.tag_resolver = proc do |controller, action, default_tag|
|
135
|
+
# # ...
|
136
|
+
# end
|
137
|
+
# > ```
|
138
|
+
#
|
125
139
|
# # Transient Settings
|
126
140
|
#
|
127
141
|
# The settings described in this section should be configured using **environment variables** and are either temporary or will become the default in the future.
|
@@ -179,6 +193,10 @@ class Rage::Configuration
|
|
179
193
|
@public_file_server ||= PublicFileServer.new
|
180
194
|
end
|
181
195
|
|
196
|
+
def openapi
|
197
|
+
@openapi ||= OpenAPI.new
|
198
|
+
end
|
199
|
+
|
182
200
|
def internal
|
183
201
|
@internal ||= Internal.new
|
184
202
|
end
|
@@ -218,6 +236,10 @@ class Rage::Configuration
|
|
218
236
|
@middlewares = (@middlewares[0..index] + [[new_middleware, args, block]] + @middlewares[index + 1..]).uniq(&:first)
|
219
237
|
end
|
220
238
|
|
239
|
+
def include?(middleware)
|
240
|
+
!!find_middleware_index(middleware) rescue false
|
241
|
+
end
|
242
|
+
|
221
243
|
private
|
222
244
|
|
223
245
|
def find_middleware_index(middleware)
|
@@ -258,12 +280,40 @@ class Rage::Configuration
|
|
258
280
|
end
|
259
281
|
end
|
260
282
|
end
|
283
|
+
|
284
|
+
def config
|
285
|
+
@config ||= begin
|
286
|
+
config_file = Rage.root.join("config/cable.yml")
|
287
|
+
|
288
|
+
if config_file.exist?
|
289
|
+
yaml = ERB.new(config_file.read).result
|
290
|
+
YAML.safe_load(yaml, aliases: true, symbolize_names: true)[Rage.env.to_sym] || {}
|
291
|
+
else
|
292
|
+
{}
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def adapter_config
|
298
|
+
config.except(:adapter)
|
299
|
+
end
|
300
|
+
|
301
|
+
def adapter
|
302
|
+
case config[:adapter]
|
303
|
+
when "redis"
|
304
|
+
Rage::Cable::Adapters::Redis.new(adapter_config)
|
305
|
+
end
|
306
|
+
end
|
261
307
|
end
|
262
308
|
|
263
309
|
class PublicFileServer
|
264
310
|
attr_accessor :enabled
|
265
311
|
end
|
266
312
|
|
313
|
+
class OpenAPI
|
314
|
+
attr_accessor :tag_resolver
|
315
|
+
end
|
316
|
+
|
267
317
|
# @private
|
268
318
|
class Internal
|
269
319
|
attr_accessor :rails_mode
|