rage-rb 1.23.0 → 1.24.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbc12eab5d13d5570107f5c7bf6d0b68f6f652eccc1eaafefdb47ef92ed3b264
4
- data.tar.gz: 7adca2c0e96d38ca69c32f4fc12b03984b6aa46578d48d084645e11891915e99
3
+ metadata.gz: b488d2ab6a7ebcc33ae77a768f55e9277b796c39c5a7451e32a823f4e7a8a848
4
+ data.tar.gz: c1e55ecf8182587ced30f1c0af834136edbd4e06d0466dfaf6685387d0ebb3d6
5
5
  SHA512:
6
- metadata.gz: cb010618b1afebee5baa5fee44963d8cdf4f1543769b79ee81d26e9a0815c3ea59d6b0cf4dad2f53adabbe8d26ae0688876d2a0f690e1497173469446ba4a9e8
7
- data.tar.gz: ee292c1c8b93fed32dbe015e6de898df1f0bb63ede69deb2b1eb41cddee1e339ade19ed8981682697721b02c3923bb8782358616c5d7c264cc0d465887b6e79b
6
+ metadata.gz: 0ed0b0e62b74d3847804613ec77da801ceb2784dda9dc81895c93c6438a07377a82a5ff3ac4099bee8d4d9698ef51e2ac12233067d0672f3b5bdf97374e2fbc6
7
+ data.tar.gz: 6b6b8d71317c165d7ff5d3a1942cd81aeeeb5978f3672fc1de32464f4d4e3ed8f10171345a11db2e3427aff4cd84868403922541701c5464ce4260d96690250b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.24.0] - 2026-05-12
4
+
5
+ ### Added
6
+
7
+ - [Deferred] Add tests for log context capture and backward-compatible restore by [@jsxs0](https://github.com/jsxs0) (#274).
8
+ - [OpenAPI] Add support for per-endpoint OAuth2/OpenID scopes via `@auth_scope` tag by [@Piyush-Goenka](https://github.com/Piyush-Goenka) (#272).
9
+ - Reuse `define_dynamic_method` and `define_maybe_yield` methods in `RageController::API` from `Rage::Internal` by [@numice](https://github.com/numice) (#273).
10
+ - Add the `form_actions` router configuration (#278).
11
+ - [Deferred] Add native periodic task scheduling with multi-process leader election via `File#flock` by [@Abishekcs](https://github.com/Abishekcs) (#233).
12
+ - [OpenAPI] Support optional attributes and `Array<>` syntax by [@ayushman1210](https://github.com/ayushman1210) (#228).
13
+ - [Errors] Add centralized error reporting interface via `Rage.errors` and `config.error_handlers` by [@Digvijay-x1](https://github.com/Digvijay-x1) (#275).
14
+
15
+ ### Fixed
16
+
17
+ - [OpenAPI] Fix SystemStackError in Alba parser with circular associations (#268).
18
+ - Rewind `rack.input` when parsing request body (#279).
19
+
20
+ ### Changed
21
+
22
+ - [Deferred] Increase default retry limit to 20 and update default retry backoff to `(attempt**4) + 10 + (rand(15) * attempt)` by [@anuj-pal27](https://github.com/anuj-pal27) (#271).
23
+ - Update `Rage::Cable` to use the new `PubSub` module (#281).
24
+
3
25
  ## [1.23.0] - 2026-04-15
4
26
 
5
27
  ### Fixed
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,240 @@
1
+ # Contributing to Rage
2
+
3
+ This guide is designed to help contributors understand the project's internals, design principles, and conventions. Whether you're fixing a bug, adding a feature, or participating in GSoC, this document will help you get started.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Design Principles](#design-principles)
8
+ - [Documentation Standards](#documentation-standards)
9
+ - [Dynamic Code Generation](#dynamic-code-generation)
10
+ - [Iodine Integration](#iodine-integration)
11
+ - [The Fiber Runtime](#the-fiber-runtime)
12
+ - [A Note on AI Usage](#a-note-on-ai-usage)
13
+
14
+ ## Design Principles
15
+
16
+ ### Performance Over Readability
17
+
18
+ Rage is a framework, not application code. While readability matters, performance takes priority when the two conflict. Framework code runs on every request, so micro-optimizations compound into significant gains.
19
+
20
+ This doesn't mean writing deliberately obscure code. It means accepting that some patterns which would be discouraged in application code are acceptable here when they improve performance.
21
+
22
+ ### Lean Happy Path
23
+
24
+ The happy path should execute as little code as possible. We achieve this through:
25
+
26
+ 1. **Boot-time computation**: Move work to server startup whenever possible. Pre-compile routes, resolve callback chains, and build method definitions during initialization rather than on each request.
27
+
28
+ 2. **Feature isolation**: New features should not impact performance for users who don't use them. If a feature requires runtime checks, consider whether those checks can be eliminated through code generation or configuration.
29
+
30
+ ### Duplication Over Premature Abstraction
31
+
32
+ Duplication is cheaper than unnecessary abstraction.
33
+
34
+ Abstractions should emerge from observed patterns, not anticipated ones. When you see similar code in two places, resist the urge to immediately extract a helper. Introducing new abstraction layers or deduplicating code should only happen after the duplication has naturally occurred and proven to be a burden.
35
+
36
+ A wrong abstraction is worse than duplicated code because:
37
+ - It's harder to understand (you must trace through multiple layers)
38
+ - It's harder to modify (changes affect all call sites)
39
+ - It's harder to remove (it becomes load-bearing)
40
+
41
+ When you do introduce an abstraction, make sure it pulls its weight.
42
+
43
+ ## Documentation Standards
44
+
45
+ ### YARD Documentation
46
+
47
+ All user-facing methods must be documented using [YARD](https://yardoc.org/). Documentation comments use Markdown formatting.
48
+
49
+ ```ruby
50
+ # Publish an event to all registered subscribers.
51
+ #
52
+ # @param event [Object] the event instance to publish
53
+ # @param context [Hash] optional context to pass to subscribers
54
+ # @return [void]
55
+ #
56
+ # @example Publishing an event
57
+ # Rage::Events.publish(OrderCreated.new(order: order))
58
+ #
59
+ # @example Publishing with context
60
+ # Rage::Events.publish(OrderCreated.new(order: order), context: { user_id: current_user.id })
61
+ #
62
+ def publish(event, context: nil)
63
+ # ...
64
+ end
65
+ ```
66
+
67
+ ### The `@private` Tag
68
+
69
+ Some methods cannot be marked `private` using Ruby's `private` keyword (e.g., they need to be called from other classes within the framework), but they are not part of the public API. These methods should be marked with the `@private` YARD tag:
70
+
71
+ ```ruby
72
+ # @private
73
+ # Used internally by the router to register controller actions.
74
+ def __register_action(action)
75
+ # ...
76
+ end
77
+ ```
78
+
79
+ The `@private` tag signals to contributors:
80
+ - This method is not part of the user-facing API
81
+ - It can be modified or removed without deprecation
82
+ - It can be used freely within the framework codebase
83
+
84
+ ## Dynamic Code Generation
85
+
86
+ Rage relies heavily on dynamic code generation for both performance and flexibility. Understanding this pattern is essential for contributing to the framework.
87
+
88
+ ### Why Dynamic Code Generation?
89
+
90
+ 1. **Performance**: Generated code avoids runtime conditionals. Instead of checking "does this controller have before actions?" on every request, we generate a method that either includes the before action calls or doesn't.
91
+
92
+ 2. **Flexibility**: Generated code can adapt to user-defined signatures, allowing optional parameters without forcing users to accept arguments they don't need.
93
+
94
+ ### Examples in the Codebase
95
+
96
+ #### Controller Action Registration
97
+
98
+ `RageController::API.__register_action` (in `lib/rage/controller/api.rb`) generates a method for each controller action at boot time:
99
+
100
+ ```ruby
101
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
102
+ def __run_#{action}
103
+ #{before_actions_chunk}
104
+ #{action} unless @__before_callback_rendered
105
+ #{after_actions_chunk}
106
+ [@__status, @__headers, @__body]
107
+ #{rescue_handlers_chunk}
108
+ end
109
+ RUBY
110
+ ```
111
+
112
+ This generates a single method that includes only the callbacks and exception handlers relevant to that specific action. No runtime resolution required.
113
+
114
+ #### Logger Rebuilding
115
+
116
+ `Rage::Logger#rebuild!` (in `lib/rage/logger/logger.rb`) generates logging methods based on the configured log level:
117
+
118
+ ```ruby
119
+ if level_val < @level
120
+ # Log level is filtered out - generate a no-op method
121
+ def info(msg = nil, context = nil)
122
+ false
123
+ end
124
+ else
125
+ # Generate a method that actually logs
126
+ def info(msg = nil, context = nil)
127
+ # ... logging implementation
128
+ end
129
+ end
130
+ ```
131
+
132
+ When logging at a level is disabled, the method becomes a no-op with zero overhead.
133
+
134
+ #### Telemetry Tracer
135
+
136
+ `Rage::Telemetry::Tracer#setup` (in `lib/rage/telemetry/tracer.rb`) generates tracing methods that call only the handlers registered for each span. If no handlers are registered, it generates a pass-through method.
137
+
138
+ ### Dynamic Keyword Arguments
139
+
140
+ One pattern Rage uses extensively is dynamic keyword arguments. This allows users to define methods that accept only the parameters they care about, without requiring `**` to absorb extras.
141
+
142
+ For example, an event subscriber can be defined either way:
143
+
144
+ ```ruby
145
+ # Subscriber that only cares about the event
146
+ def call(event)
147
+ end
148
+
149
+ # Subscriber that also needs context
150
+ def call(event, context:)
151
+ end
152
+ ```
153
+
154
+ Both work regardless of whether the event was published with context. The framework inspects the method signature and generates a call that passes only the expected arguments.
155
+
156
+ The `Rage::Internal.build_arguments` method (in `lib/rage/internal.rb`) implements this pattern:
157
+
158
+ ```ruby
159
+ def build_arguments(method, arguments)
160
+ expected_parameters = method.parameters
161
+
162
+ arguments.filter_map { |arg_name, arg_value|
163
+ if expected_parameters.any? { |param_type, param_name| param_name == arg_name || param_type == :keyrest }
164
+ "#{arg_name}: #{arg_value}"
165
+ end
166
+ }.join(", ")
167
+ end
168
+ ```
169
+
170
+ This inspects the target method's parameters and generates a string containing only the arguments that method expects. The generated string is then embedded into dynamically defined code.
171
+
172
+ This pattern appears in:
173
+ - Event subscribers (accepting event with optional context)
174
+ - Telemetry handlers (accepting various span attributes)
175
+ - External loggers (accepting severity, message, context, etc.)
176
+
177
+ ## Iodine Integration
178
+
179
+ Rage consists of two components: the framework (this repository) and its server, [Iodine](https://github.com/rage-rb/iodine). Iodine is not an external dependency; it's part of the Rage runtime, and its methods can be used freely within the codebase.
180
+
181
+ ### Useful Iodine Methods
182
+
183
+ **`Iodine.run_after(milliseconds) { ... }`**: Schedule a block to run after a delay.
184
+
185
+ ```ruby
186
+ Iodine.run_after(5000) do
187
+ cleanup_expired_sessions
188
+ end
189
+ ```
190
+
191
+ **`Iodine.run_every(milliseconds) { ... }`**: Schedule a block to run at regular intervals.
192
+
193
+ ```ruby
194
+ Iodine.run_every(60_000) do
195
+ report_metrics
196
+ end
197
+ ```
198
+
199
+ **`Iodine.publish(channel, message, engine)`**: Send a message to subscribers. This is used for inter-fiber and inter-process communication.
200
+
201
+ ```ruby
202
+ # Notify within the current process
203
+ Iodine.publish("my_channel", "message", Iodine::PubSub::PROCESS)
204
+
205
+ # Notify across all processes in the cluster
206
+ Iodine.publish("my_channel", "message", Iodine::PubSub::CLUSTER)
207
+ ```
208
+
209
+ **`Iodine.on_state(state) { ... }`**: Register callbacks for server lifecycle events.
210
+
211
+ ```ruby
212
+ Iodine.on_state(:on_start) do
213
+ # Runs when the worker process starts
214
+ end
215
+ ```
216
+
217
+ ## The Fiber Runtime
218
+
219
+ ### Rage::FiberWrapper
220
+
221
+ `Rage::FiberWrapper` (in `lib/rage/middleware/fiber_wrapper.rb`) is the glue between the framework and the server. It sits at the top of the middleware stack and:
222
+
223
+ 1. Wraps every request in a Fiber
224
+ 2. Implements the defer protocol for pausing/resuming async requests
225
+
226
+ When a request encounters blocking I/O (database query, HTTP request, etc.), the fiber yields. `FiberWrapper` detects this (`fiber.alive?`) and returns a special `:__http_defer__` signal to Iodine, which pauses the connection.
227
+
228
+ When the I/O completes, the fiber resumes and publishes a message to notify Iodine that the response is ready. This is the mechanism that enables transparent, non-blocking concurrency.
229
+
230
+ ## A Note on AI Usage
231
+
232
+ Contributors are free to use AI tools however they see fit.
233
+
234
+ One thing to keep in mind: when delegating development to AI, the friction this removes is the very friction that enables developers to understand the system, learn, and grow as professionals.
235
+
236
+ There's value in the struggle of tracing through code, understanding why something was designed a certain way, and building mental models of complex systems. Use AI to assist and accelerate, but ensure you are still engaging deeply with the architecture and the "why" behind the code you are committing.
237
+
238
+ ---
239
+
240
+ Questions? Open an issue or reach out to the maintainers.
data/README.md CHANGED
@@ -206,6 +206,7 @@ Rage is a good fit if you:
206
206
  - Documentation: [https://rage-rb.dev](https://rage-rb.dev/docs/intro)
207
207
  - API Reference: [https://rage-rb.dev/api](https://rage-rb.dev/api)
208
208
  - Architecture: [ARCHITECTURE.md](https://github.com/rage-rb/rage/blob/main/ARCHITECTURE.md)
209
+ - Contributing: [CONTRIBUTING.md](https://github.com/rage-rb/rage/blob/main/CONTRIBUTING.md)
209
210
 
210
211
  Contributions and thoughtful feedback are welcome.
211
212
 
@@ -23,6 +23,7 @@ class Rage::Application
23
23
  response = @exception_app.call(400, e)
24
24
 
25
25
  rescue Exception => e
26
+ Rage::Errors.report(e)
26
27
  response = @exception_app.call(500, e)
27
28
 
28
29
  ensure
@@ -29,6 +29,9 @@
29
29
  # ```
30
30
  #
31
31
  module Rage::Cable
32
+ PUBSUB_BROADCASTER_ID = "cable"
33
+ private_constant :PUBSUB_BROADCASTER_ID
34
+
32
35
  # Create a new Cable application.
33
36
  #
34
37
  # @example
@@ -36,9 +39,6 @@ module Rage::Cable
36
39
  # run Rage.cable.application
37
40
  # end
38
41
  def self.application
39
- # explicitly initialize the adapter
40
- __adapter
41
-
42
42
  handler = __build_handler(__protocol)
43
43
  accept_response = [0, __protocol.protocol_definition, []]
44
44
 
@@ -61,6 +61,14 @@ module Rage::Cable
61
61
  chain
62
62
  end
63
63
 
64
+ # @private
65
+ def self.__initialize
66
+ if (adapter = Rage.config.pubsub.adapter)
67
+ adapter.add_broadcaster(PUBSUB_BROADCASTER_ID, __protocol)
68
+ @__adapter = adapter
69
+ end
70
+ end
71
+
64
72
  # @private
65
73
  def self.__router
66
74
  @__router ||= Router.new
@@ -71,11 +79,6 @@ module Rage::Cable
71
79
  @__protocol ||= Rage.config.cable.protocol.tap { |protocol| protocol.init(__router) }
72
80
  end
73
81
 
74
- # @private
75
- def self.__adapter
76
- @__adapter ||= Rage.config.cable.adapter
77
- end
78
-
79
82
  # @private
80
83
  def self.__build_handler(protocol)
81
84
  klass = Class.new do
@@ -125,7 +128,8 @@ module Rage::Cable
125
128
  end
126
129
 
127
130
  def log_error(e)
128
- Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
131
+ Rage.logger.error("Unhandled exception has occurred - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
132
+ Rage::Errors.report(e)
129
133
  end
130
134
  end
131
135
 
@@ -142,7 +146,7 @@ module Rage::Cable
142
146
  def self.broadcast(stream, data)
143
147
  Rage::Telemetry.tracer.span_cable_stream_broadcast(stream:) do
144
148
  __protocol.broadcast(stream, data)
145
- __adapter&.publish(stream, data)
149
+ @__adapter&.publish(PUBSUB_BROADCASTER_ID, stream, data)
146
150
  end
147
151
 
148
152
  true
@@ -174,11 +178,6 @@ module Rage::Cable
174
178
  # end
175
179
  # end
176
180
 
177
- module Adapters
178
- autoload :Base, "rage/cable/adapters/base"
179
- autoload :Redis, "rage/cable/adapters/redis"
180
- end
181
-
182
181
  module Protocols
183
182
  end
184
183
 
@@ -191,3 +190,9 @@ require_relative "protocols/raw_web_socket_json"
191
190
  require_relative "channel"
192
191
  require_relative "connection"
193
192
  require_relative "router"
193
+
194
+ if Rage.config.internal.initialized?
195
+ Rage::Cable.__initialize
196
+ else
197
+ Rage.config.after_initialize { Rage::Cable.__initialize }
198
+ end
@@ -331,7 +331,8 @@ class Rage::Cable::Channel
331
331
  Fiber.schedule do
332
332
  slice.each { |channel| callback.call(channel) }
333
333
  rescue => e
334
- Rage.logger.error("Unhandled exception has occured - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
334
+ Rage.logger.error("Unhandled exception has occurred - #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
335
+ Rage::Errors.report(e)
335
336
  end
336
337
  end
337
338
  end
@@ -219,6 +219,14 @@ class Rage::Configuration
219
219
  end
220
220
  # @!endgroup
221
221
 
222
+ # @!group Error Reporter Configuration
223
+ # Allows configuring error reporters.
224
+ # @return [Rage::Configuration::ErrorReporters]
225
+ def error_reporters
226
+ @error_reporters ||= ErrorReporters.new
227
+ end
228
+ # @!endgroup
229
+
222
230
  # @!group OpenAPI Configuration
223
231
  # Allows configuring OpenAPI settings.
224
232
  # @return [Rage::Configuration::OpenAPI]
@@ -265,6 +273,14 @@ class Rage::Configuration
265
273
  end
266
274
  # @!endgroup
267
275
 
276
+ # @!group Router Configuration
277
+ # Allows configuring router settings.
278
+ # @return [Rage::Configuration::Router]
279
+ def router
280
+ @router ||= Router.new
281
+ end
282
+ # @!endgroup
283
+
268
284
  # @private
269
285
  def pubsub
270
286
  @pubsub ||= PubSub.new
@@ -378,6 +394,60 @@ class Rage::Configuration
378
394
  end
379
395
  end
380
396
 
397
+ class ErrorReporters
398
+ # @private
399
+ def initialize
400
+ @objects = []
401
+ end
402
+
403
+ # @private
404
+ def objects
405
+ @objects.dup
406
+ end
407
+
408
+ # Add a new error reporter.
409
+ # Error reporters should respond to `#call` and accept one of:
410
+ # - `call(exception)`
411
+ # - `call(exception, context: {})`
412
+ #
413
+ # @param reporter [#call]
414
+ # @return [self]
415
+ # @example
416
+ # Rage.configure do
417
+ # config.error_reporters << SentryReporter.new
418
+ # end
419
+ def <<(reporter)
420
+ validate_input!(reporter)
421
+ return self if @objects.include?(reporter)
422
+
423
+ @objects << reporter
424
+ Rage::Errors.__send__(:__register_reporter, reporter)
425
+
426
+ self
427
+ end
428
+
429
+ alias_method :push, :<<
430
+
431
+ # Remove an error reporter.
432
+ # @param reporter [#call] the reporter to remove
433
+ # @example
434
+ # reporter = SentryReporter.new
435
+ # Rage.configure do
436
+ # config.error_reporters.delete(reporter)
437
+ # end
438
+ def delete(reporter)
439
+ deleted = @objects.delete(reporter)
440
+ Rage::Errors.__send__(:__unregister_reporter, reporter) if deleted
441
+ deleted
442
+ end
443
+
444
+ private
445
+
446
+ def validate_input!(reporter)
447
+ raise ArgumentError, "error reporter must respond to #call" unless reporter.respond_to?(:call)
448
+ end
449
+ end
450
+
381
451
  class Server
382
452
  # @!attribute port
383
453
  # Specify the port the server will listen on.
@@ -632,33 +702,6 @@ class Rage::Configuration
632
702
  end
633
703
  end
634
704
  end
635
-
636
- # @private
637
- def config
638
- @config ||= begin
639
- config_file = Rage.root.join("config/cable.yml")
640
-
641
- if config_file.exist?
642
- yaml = ERB.new(config_file.read).result
643
- YAML.safe_load(yaml, aliases: true, symbolize_names: true)[Rage.env.to_sym] || {}
644
- else
645
- {}
646
- end
647
- end
648
- end
649
-
650
- # @private
651
- def adapter_config
652
- config.except(:adapter)
653
- end
654
-
655
- # @private
656
- def adapter
657
- case config[:adapter]
658
- when "redis"
659
- Rage::Cable::Adapters::Redis.new(adapter_config)
660
- end
661
- end
662
705
  end
663
706
 
664
707
  class PublicFileServer
@@ -707,6 +750,46 @@ class Rage::Configuration
707
750
  # @private
708
751
  def initialize
709
752
  @configured = false
753
+ @schedule_blocks = []
754
+ end
755
+
756
+ # Schedule a periodic task to run at a fixed interval.
757
+ # @example
758
+ # Rage.configure do
759
+ # config.deferred.schedule do
760
+ # every 5.minutes, task: ClearCache
761
+ # end
762
+ # end
763
+ def schedule(&block)
764
+ @schedule_blocks << block
765
+ end
766
+
767
+ # @private
768
+ # Evaluates all stored schedule blocks and returns the collected tasks.
769
+ # Called at boot time after all app constants are loaded.
770
+ def scheduled_tasks
771
+ @schedule_blocks.flat_map do |block|
772
+ dsl = ScheduleDSL.new
773
+ dsl.instance_eval(&block)
774
+ dsl.tasks
775
+ end
776
+ end
777
+
778
+ # @private
779
+ class ScheduleDSL
780
+ attr_reader :tasks
781
+
782
+ def initialize
783
+ @tasks = []
784
+ end
785
+
786
+ # Registers a task to run on a fixed interval (in seconds)
787
+ def every(interval, task:)
788
+ unless task.is_a?(Class) && task.include?(Rage::Deferred::Task)
789
+ raise ArgumentError, "#{task} must be a class that includes Rage::Deferred::Task"
790
+ end
791
+ @tasks << { interval:, task: }
792
+ end
710
793
  end
711
794
 
712
795
  # Returns the backend instance used by `Rage::Deferred`.
@@ -988,6 +1071,17 @@ class Rage::Configuration
988
1071
  attr_accessor :key
989
1072
  end
990
1073
 
1074
+ class Router
1075
+ # @!attribute form_actions
1076
+ # Enable the automatic generation of `new` and `edit` routes via resource helpers.
1077
+ # @return [Boolean]
1078
+ # @example Enable form actions
1079
+ # Rage.configure do
1080
+ # config.router.form_actions = true
1081
+ # end
1082
+ attr_accessor :form_actions
1083
+ end
1084
+
991
1085
  # @private
992
1086
  class PubSub
993
1087
  attr_reader :adapter
@@ -1004,6 +1098,7 @@ class Rage::Configuration
1004
1098
  def config
1005
1099
  @config ||= begin
1006
1100
  config_file = Rage.root.join("config/pubsub.yml")
1101
+ config_file = Rage.root.join("config/cable.yml") unless config_file.exist?
1007
1102
 
1008
1103
  config = if config_file.exist?
1009
1104
  yaml = ERB.new(config_file.read).result
@@ -185,32 +185,6 @@ class RageController::API
185
185
  klass.__wrap_parameters_options = __wrap_parameters_options
186
186
  end
187
187
 
188
- # @private
189
- @@__dynamic_name_seed = ("a".."i").to_a.permutation
190
-
191
- # @private
192
- # define a method based on a block
193
- def define_dynamic_method(block)
194
- name = @@__dynamic_name_seed.next.join
195
- define_method("__rage_dynamic_#{name}", block)
196
- end
197
-
198
- # @private
199
- # define a method that will call a specified method if a condition is `true` or yield if `false`
200
- def define_maybe_yield(method_name)
201
- name = @@__dynamic_name_seed.next.join
202
-
203
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
204
- def __rage_dynamic_#{name}(condition)
205
- if condition
206
- #{method_name} { yield }
207
- else
208
- yield
209
- end
210
- end
211
- RUBY
212
- end
213
-
214
188
  # @private
215
189
  def __register_renderer(name, block)
216
190
  prepend(RageController::Renderers) unless ancestors.include?(RageController::Renderers)
@@ -240,7 +214,7 @@ class RageController::API
240
214
  def rescue_from(*klasses, with: nil, &block)
241
215
  unless with
242
216
  if block_given?
243
- with = define_dynamic_method(block)
217
+ with = Rage::Internal.define_dynamic_method(self, block)
244
218
  else
245
219
  raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
246
220
  end
@@ -314,7 +288,7 @@ class RageController::API
314
288
  # end
315
289
  def around_action(action_name = nil, **opts, &block)
316
290
  action = prepare_action_params(action_name, **opts, &block)
317
- action.merge!(around: true, wrapper: define_maybe_yield(action[:name]))
291
+ action.merge!(around: true, wrapper: Rage::Internal.define_maybe_yield(self, action[:name]))
318
292
 
319
293
  if @__before_actions && @__before_actions.frozen?
320
294
  @__before_actions = @__before_actions.dup
@@ -429,7 +403,7 @@ class RageController::API
429
403
  # used by `before_action` and `after_action`
430
404
  def prepare_action_params(action_name = nil, **opts, &block)
431
405
  if block_given?
432
- action_name = define_dynamic_method(block)
406
+ action_name = Rage::Internal.define_dynamic_method(self, block)
433
407
  elsif action_name.nil?
434
408
  raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
435
409
  end
@@ -444,8 +418,8 @@ class RageController::API
444
418
  unless: _unless
445
419
  }
446
420
 
447
- action[:if] = define_dynamic_method(action[:if]) if action[:if].is_a?(Proc)
448
- action[:unless] = define_dynamic_method(action[:unless]) if action[:unless].is_a?(Proc)
421
+ action[:if] = Rage::Internal.define_dynamic_method(self, action[:if]) if action[:if].is_a?(Proc)
422
+ action[:unless] = Rage::Internal.define_dynamic_method(self, action[:unless]) if action[:unless].is_a?(Proc)
449
423
 
450
424
  action
451
425
  end
@@ -82,10 +82,16 @@ module Rage::Deferred
82
82
  )
83
83
  end
84
84
 
85
+ # @private
86
+ def self.__start_scheduler
87
+ Rage::Deferred::Scheduler.start(Rage.config.deferred.scheduled_tasks)
88
+ end
89
+
85
90
  # @private
86
91
  def self.__initialize
87
92
  __middleware_chain
88
93
  __load_tasks
94
+ __start_scheduler
89
95
  end
90
96
 
91
97
  module Backends
@@ -96,6 +102,7 @@ module Rage::Deferred
96
102
  end
97
103
 
98
104
  require_relative "task"
105
+ require_relative "scheduler"
99
106
  require_relative "queue"
100
107
  require_relative "proxy"
101
108
  require_relative "context"