rage-rb 1.17.1 → 1.19.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/ARCHITECTURE.md +47 -0
- data/CHANGELOG.md +23 -0
- data/lib/rage/all.rb +1 -0
- data/lib/rage/application.rb +3 -25
- data/lib/rage/cable/cable.rb +2 -3
- data/lib/rage/cli.rb +101 -18
- data/lib/rage/code_loader.rb +8 -0
- data/lib/rage/configuration.rb +525 -195
- data/lib/rage/controller/api.rb +15 -3
- data/lib/rage/deferred/context.rb +48 -0
- data/lib/rage/deferred/deferred.rb +5 -1
- data/lib/rage/deferred/queue.rb +8 -8
- data/lib/rage/deferred/task.rb +29 -23
- data/lib/rage/env.rb +15 -0
- data/lib/rage/events/events.rb +140 -0
- data/lib/rage/events/subscriber.rb +174 -0
- data/lib/rage/fiber.rb +11 -2
- data/lib/rage/fiber_scheduler.rb +2 -2
- data/lib/rage/hooks.rb +1 -0
- data/lib/rage/internal.rb +53 -0
- data/lib/rage/log_processor.rb +117 -0
- data/lib/rage/logger/json_formatter.rb +37 -18
- data/lib/rage/logger/logger.rb +136 -30
- data/lib/rage/logger/text_formatter.rb +21 -2
- data/lib/rage/middleware/fiber_wrapper.rb +8 -0
- data/lib/rage/middleware/reloader.rb +6 -11
- data/lib/rage/request.rb +18 -1
- data/lib/rage/response.rb +1 -1
- data/lib/rage/router/util.rb +8 -0
- data/lib/rage/setup.rb +2 -0
- data/lib/rage/templates/config-environments-production.rb +1 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +51 -0
- data/rage.gemspec +1 -0
- metadata +22 -4
- data/OVERVIEW.md +0 -83
- data/lib/rage/deferred/metadata.rb +0 -43
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 847e3debe188b3b34ffdd6b7306edeca0a053b82c0e5106b3faa37a6e43f20bf
|
|
4
|
+
data.tar.gz: 202db65e0a17d27ae94178d93ddfca940a95237450095b06c0fa4e080a6fd09a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74610482cca2fd1394e3ffd4bd2cf577ed78a980140e39bf359ece4050bed7497dc1845a472a3c403a6fb0cbd48c1124a1714b8e7c8352b0ed720287177418c9
|
|
7
|
+
data.tar.gz: cd7cec704b0019012d1a08aea63b97dd83e0a6ee17155228d9517575e7882482c58b4b227d6663b053cc77a2edc551e34da53d0bf9bc0463e254ca917f9e8af4
|
data/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
### Table of Contents
|
|
2
|
+
|
|
3
|
+
[API Workflow](#api-workflow)<br>
|
|
4
|
+
[Executing Controller Actions](#executing-controller-actions)<br>
|
|
5
|
+
[Cable Workflow](#cable-workflow)<br>
|
|
6
|
+
[OpenAPI Workflow](#openapi-workflow)<br>
|
|
7
|
+
[Design Principles](#design-principles)<br>
|
|
8
|
+
|
|
9
|
+
### API Workflow
|
|
10
|
+
|
|
11
|
+
The following diagram describes some of Rage's internal components and the way they interact with each other:
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
### Executing Controller Actions
|
|
16
|
+
|
|
17
|
+
To maximize runtime performance, Rage pre-compiles controller actions when the application boots. For each action, it resolves the full chain of callbacks and exception handlers, building a single, optimized procedure.
|
|
18
|
+
|
|
19
|
+
When a request comes in, Rage executes this pre-compiled procedure directly, avoiding the overhead of resolving callbacks and exception handlers on every request. All of this happens at boot time to ensure the request-response cycle is as fast as possible.
|
|
20
|
+
|
|
21
|
+
### Cable Workflow
|
|
22
|
+
|
|
23
|
+
`Rage::Cable` provides a component for handling real-time communication over WebSockets. The workflow involves authenticating connections and subscribing them to channels for bidirectional messaging.
|
|
24
|
+
|
|
25
|
+
The following diagram describes the components of a `Rage::Cable` application:
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
### OpenAPI Workflow
|
|
30
|
+
|
|
31
|
+
`Rage::OpenAPI` generates OpenAPI 3.0 specifications by parsing comments in controller files. This process happens at boot time, building the specification and storing it in memory to be served for API documentation.
|
|
32
|
+
|
|
33
|
+
The following diagram describes the flow of `Rage::OpenAPI`:
|
|
34
|
+
|
|
35
|
+
<img width="800" src="https://github.com/user-attachments/assets/b4a87b1e-9a0f-4432-a3e9-0106ff546f3f" />
|
|
36
|
+
|
|
37
|
+
### Design Principles
|
|
38
|
+
|
|
39
|
+
* **Lean Happy Path:** We try to execute as many operations as possible during server initialization to minimize workload during request processing. Additionally, new features should be designed to avoid impacting the framework performance for users who do not utilize those features.
|
|
40
|
+
|
|
41
|
+
* **Performance Over Code Style:** We recognize that framework and application code have different requirements. While testability and readability are crucial for application code, framework code prioritizes performance and careful abstraction. This allows for future modifications while maintaining backward compatibility, though readability remains important.
|
|
42
|
+
|
|
43
|
+
* **Rails Compatibility:** A key objective is to provide a familiar experience for Rails developers, with the Controller and Cable APIs being largely compatible. However, Rage is not a reimplementation of Rails. Instead, it provides a familiar foundation and builds upon it with its own unique features.
|
|
44
|
+
|
|
45
|
+
* **Idiomatic Ruby:** We prioritize idiomatic Ruby, avoiding unnecessary abstractions. User-level code is expected to embrace standard Ruby syntax, approaches, and patterns, as this is preferable to a framework-level abstraction that accomplishes the same task.
|
|
46
|
+
|
|
47
|
+
* **Single-Threaded Fiber-Based Approach:** Each request is processed in a separate, isolated execution context (Fiber), pausing whenever it encounters blocking I/O. The single-threaded approach eliminates thread synchronization overhead, leading to enhanced performance and simplified code.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.19.0] - 2025-12-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add ability to specify external loggers (#178).
|
|
8
|
+
- Pass all of log data to deferred tasks (#173).
|
|
9
|
+
- Add the `Request#route_uri_pattern` method (#175).
|
|
10
|
+
- Support global log tags and context (#171, #177).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fix reloading in dev with user-level fibers (#170).
|
|
15
|
+
|
|
16
|
+
## [1.18.0] - 2025-10-29
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Add `Rage::Events` (#167).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fix sequential `Fiber.await` calls (#168).
|
|
25
|
+
|
|
3
26
|
## [1.17.1] - 2025-08-21
|
|
4
27
|
|
|
5
28
|
### Fixed
|
data/lib/rage/all.rb
CHANGED
data/lib/rage/application.rb
CHANGED
|
@@ -4,10 +4,11 @@ class Rage::Application
|
|
|
4
4
|
def initialize(router)
|
|
5
5
|
@router = router
|
|
6
6
|
@exception_app = build_exception_app
|
|
7
|
+
@log_processor = Rage.__log_processor
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def call(env)
|
|
10
|
-
|
|
11
|
+
@log_processor.init_request_logger(env)
|
|
11
12
|
|
|
12
13
|
handler = @router.lookup(env)
|
|
13
14
|
|
|
@@ -25,34 +26,11 @@ class Rage::Application
|
|
|
25
26
|
response = @exception_app.call(500, e)
|
|
26
27
|
|
|
27
28
|
ensure
|
|
28
|
-
|
|
29
|
+
@log_processor.finalize_request_logger(env, response, params)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
private
|
|
32
33
|
|
|
33
|
-
DEFAULT_LOG_CONTEXT = {}.freeze
|
|
34
|
-
private_constant :DEFAULT_LOG_CONTEXT
|
|
35
|
-
|
|
36
|
-
def init_logger(env)
|
|
37
|
-
Thread.current[:rage_logger] = {
|
|
38
|
-
tags: [(env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag)],
|
|
39
|
-
context: DEFAULT_LOG_CONTEXT,
|
|
40
|
-
request_start: Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
41
|
-
}
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def finalize_logger(env, response, params)
|
|
45
|
-
logger = Thread.current[:rage_logger]
|
|
46
|
-
|
|
47
|
-
duration = (
|
|
48
|
-
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger[:request_start]) * 1000
|
|
49
|
-
).round(2)
|
|
50
|
-
|
|
51
|
-
logger[:final] = { env:, params:, response:, duration: }
|
|
52
|
-
Rage.logger.info("")
|
|
53
|
-
logger[:final] = nil
|
|
54
|
-
end
|
|
55
|
-
|
|
56
34
|
def build_exception_app
|
|
57
35
|
if Rage.env.development?
|
|
58
36
|
->(status, e) do
|
data/lib/rage/cable/cable.rb
CHANGED
|
@@ -52,11 +52,10 @@ module Rage::Cable
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
@protocol = protocol
|
|
55
|
-
@
|
|
55
|
+
@log_processor = Rage.__log_processor
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def on_open(connection)
|
|
59
|
-
connection.env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag
|
|
60
59
|
schedule_fiber(connection) { @protocol.on_open(connection) }
|
|
61
60
|
end
|
|
62
61
|
|
|
@@ -83,7 +82,7 @@ module Rage::Cable
|
|
|
83
82
|
|
|
84
83
|
def schedule_fiber(connection)
|
|
85
84
|
Fiber.schedule do
|
|
86
|
-
|
|
85
|
+
@log_processor.init_request_logger(connection.env)
|
|
87
86
|
yield
|
|
88
87
|
rescue => e
|
|
89
88
|
log_error(e)
|
data/lib/rage/cli.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Rage
|
|
|
12
12
|
File.expand_path("templates", __dir__)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
desc "migration NAME", "Generate a new migration
|
|
15
|
+
desc "migration NAME", "Generate a new migration"
|
|
16
16
|
def migration(name = nil)
|
|
17
17
|
return help("migration") if name.nil?
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ module Rage
|
|
|
20
20
|
Rake::Task["db:new_migration"].invoke(name)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
desc "model NAME", "Generate a new model
|
|
23
|
+
desc "model NAME", "Generate a new model"
|
|
24
24
|
def model(name = nil)
|
|
25
25
|
return help("model") if name.nil?
|
|
26
26
|
|
|
@@ -30,7 +30,7 @@ module Rage
|
|
|
30
30
|
template("model-template/model.rb", "app/models/#{name.singularize.underscore}.rb")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
desc "controller NAME", "Generate a new controller
|
|
33
|
+
desc "controller NAME", "Generate a new controller"
|
|
34
34
|
def controller(name = nil)
|
|
35
35
|
return help("controller") if name.nil?
|
|
36
36
|
|
|
@@ -65,9 +65,9 @@ module Rage
|
|
|
65
65
|
true
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
desc "new PATH", "Create a new application
|
|
69
|
-
option :database, aliases: "-d", desc: "Preconfigure for selected database
|
|
70
|
-
option :help, aliases: "-h", desc: "Show this message
|
|
68
|
+
desc "new PATH", "Create a new application"
|
|
69
|
+
option :database, aliases: "-d", desc: "Preconfigure for selected database", enum: %w(mysql trilogy postgresql sqlite3)
|
|
70
|
+
option :help, aliases: "-h", desc: "Show this message"
|
|
71
71
|
def new(path = nil)
|
|
72
72
|
return help("new") if options.help? || path.nil?
|
|
73
73
|
|
|
@@ -75,12 +75,12 @@ module Rage
|
|
|
75
75
|
CLINewAppGenerator.start([path, options[:database]])
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
desc "s", "Start the app server
|
|
79
|
-
option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000
|
|
80
|
-
option :environment, aliases: "-e", desc: "Specifies the environment to run this server under (test/development/production)
|
|
81
|
-
option :binding, aliases: "-b", desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments
|
|
82
|
-
option :config, aliases: "-c", desc: "Uses a custom rack configuration
|
|
83
|
-
option :help, aliases: "-h", desc: "Show this message
|
|
78
|
+
desc "s", "Start the app server"
|
|
79
|
+
option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000"
|
|
80
|
+
option :environment, aliases: "-e", desc: "Specifies the environment to run this server under (test/development/production)"
|
|
81
|
+
option :binding, aliases: "-b", desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments"
|
|
82
|
+
option :config, aliases: "-c", desc: "Uses a custom rack configuration"
|
|
83
|
+
option :help, aliases: "-h", desc: "Show this message"
|
|
84
84
|
def server
|
|
85
85
|
return help("server") if options.help?
|
|
86
86
|
|
|
@@ -104,9 +104,9 @@ module Rage
|
|
|
104
104
|
::Iodine.start
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
desc "routes", "List all routes
|
|
107
|
+
desc "routes", "List all routes"
|
|
108
108
|
option :grep, aliases: "-g", desc: "Filter routes by pattern"
|
|
109
|
-
option :help, aliases: "-h", desc: "Show this message
|
|
109
|
+
option :help, aliases: "-h", desc: "Show this message"
|
|
110
110
|
def routes
|
|
111
111
|
return help("routes") if options.help?
|
|
112
112
|
# the result would be something like this:
|
|
@@ -162,8 +162,8 @@ module Rage
|
|
|
162
162
|
end
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
-
desc "c", "Start the app console
|
|
166
|
-
option :help, aliases: "-h", desc: "Show this message
|
|
165
|
+
desc "c", "Start the app console"
|
|
166
|
+
option :help, aliases: "-h", desc: "Show this message"
|
|
167
167
|
def console
|
|
168
168
|
return help("console") if options.help?
|
|
169
169
|
|
|
@@ -185,17 +185,49 @@ module Rage
|
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
desc "events [EVENT1, EVENT2]", "List all registered events and their subscribers"
|
|
189
|
+
option :help, aliases: "-h", desc: "Show this message"
|
|
190
|
+
def events(*event_class_names)
|
|
191
|
+
return help("events") if options.help?
|
|
192
|
+
|
|
193
|
+
environment
|
|
194
|
+
Rage::Events.__eager_load_subscribers if Rage.env.development?
|
|
195
|
+
|
|
196
|
+
event_classes = if event_class_names.any?
|
|
197
|
+
event_class_names.flat_map { |name| name.split(",") }.map do |event_class_name|
|
|
198
|
+
@last_event_class_name = event_class_name
|
|
199
|
+
Object.const_get(event_class_name)
|
|
200
|
+
end
|
|
201
|
+
else
|
|
202
|
+
registered_events = Rage::Events.__registered_subscribers.keys
|
|
203
|
+
registered_events.reject do |event_class|
|
|
204
|
+
registered_events.any? { |e| e.ancestors.include?(event_class) && e.ancestors.index(event_class) != 0 }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
event_classes.each { |event_class| print_event_subscribers_tree(event_class) }
|
|
209
|
+
|
|
210
|
+
rescue NameError
|
|
211
|
+
if @last_event_class_name
|
|
212
|
+
spell_checker = DidYouMean::SpellChecker.new(dictionary: Rage::Events.__registered_subscribers.keys)
|
|
213
|
+
suggestion = DidYouMean.formatter.message_for(spell_checker.correct(@last_event_class_name))
|
|
214
|
+
puts "Could not find the `#{@last_event_class_name}` event. #{suggestion}"
|
|
215
|
+
else
|
|
216
|
+
raise
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
188
220
|
desc "version", "Return the current version of the framework"
|
|
189
221
|
def version
|
|
190
222
|
puts Rage::VERSION
|
|
191
223
|
end
|
|
192
224
|
|
|
193
225
|
map "generate" => :g
|
|
194
|
-
desc "g TYPE", "Generate new code
|
|
226
|
+
desc "g TYPE", "Generate new code"
|
|
195
227
|
subcommand "g", CLICodeGenerator
|
|
196
228
|
|
|
197
229
|
map "--tasks" => :tasks
|
|
198
|
-
desc "--tasks", "See the list of available tasks
|
|
230
|
+
desc "--tasks", "See the list of available tasks"
|
|
199
231
|
def tasks
|
|
200
232
|
require "io/console"
|
|
201
233
|
|
|
@@ -275,6 +307,57 @@ module Rage
|
|
|
275
307
|
end
|
|
276
308
|
end
|
|
277
309
|
end
|
|
310
|
+
|
|
311
|
+
def print_event_subscribers_tree(event_class)
|
|
312
|
+
subscribers = Rage::Events.__get_subscribers(event_class)
|
|
313
|
+
|
|
314
|
+
event_ancestors = event_class.ancestors.take_while { |klass| klass != Struct && klass != Data && klass != Object }
|
|
315
|
+
|
|
316
|
+
# build a tree of all events and their subscribers
|
|
317
|
+
tree = event_ancestors.each_with_object({}) do |ancestor, memo|
|
|
318
|
+
level = event_class.ancestors.count { |klass| klass.ancestors.include?(ancestor) } - 1
|
|
319
|
+
filtered_subscribers = subscribers.select { |subscriber| subscriber.__event_classes.include?(ancestor) }
|
|
320
|
+
|
|
321
|
+
memo[ancestor] = { level:, subscribers: filtered_subscribers }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# reject events without subscribers located on the last levels
|
|
325
|
+
i = 0
|
|
326
|
+
tree = tree.reject do |_, node|
|
|
327
|
+
level, subscribers = node[:level], node[:subscribers]
|
|
328
|
+
next_level = tree.values.dig(i + 1, :level)
|
|
329
|
+
i += 1
|
|
330
|
+
|
|
331
|
+
(next_level.nil? || next_level < level) && subscribers.empty?
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# indentation for each level
|
|
335
|
+
padding = " " * 3
|
|
336
|
+
|
|
337
|
+
# print the tree
|
|
338
|
+
tree.each_with_index do |(event_ancestor, node), i|
|
|
339
|
+
level, subscribers = node[:level], node[:subscribers]
|
|
340
|
+
next_level = tree.values.dig(i + 1, :level)
|
|
341
|
+
|
|
342
|
+
prefix = if i > 0 && next_level != level
|
|
343
|
+
"└─"
|
|
344
|
+
else
|
|
345
|
+
"├─"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
event_class_line = "#{padding * level}#{prefix} \e[90m#{event_ancestor}\e[0m"
|
|
349
|
+
if level == 0
|
|
350
|
+
puts event_class_line
|
|
351
|
+
else
|
|
352
|
+
puts "|#{event_class_line}"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
subscribers.each do |subscriber|
|
|
356
|
+
prefix = subscriber == subscribers.last && next_level != level + 1 ? "└─" : "├─"
|
|
357
|
+
puts "│#{padding * (level + 1)}#{prefix} \e[1m#{subscriber}\e[0m"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
278
361
|
end
|
|
279
362
|
|
|
280
363
|
class CLINewAppGenerator < Thor::Group
|
data/lib/rage/code_loader.rb
CHANGED
|
@@ -64,6 +64,10 @@ class Rage::CodeLoader
|
|
|
64
64
|
@last_watched, @last_update_at = current_watched, current_update_at
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
def load_file(path)
|
|
68
|
+
@loader.load_file(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
67
71
|
private
|
|
68
72
|
|
|
69
73
|
def configure_components
|
|
@@ -83,5 +87,9 @@ class Rage::CodeLoader
|
|
|
83
87
|
unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
|
|
84
88
|
Rage::OpenAPI.__reset_data_cache
|
|
85
89
|
end
|
|
90
|
+
|
|
91
|
+
unless Rage.autoload?(:Events) # the `Events` component is loaded
|
|
92
|
+
Rage::Events.__reset_subscribers
|
|
93
|
+
end
|
|
86
94
|
end
|
|
87
95
|
end
|