rage-rb 1.23.0 → 1.25.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/CONTRIBUTING.md +240 -0
  4. data/README.md +66 -123
  5. data/lib/rage/application.rb +1 -0
  6. data/lib/rage/cable/cable.rb +20 -15
  7. data/lib/rage/cable/channel.rb +2 -1
  8. data/lib/rage/configuration.rb +166 -29
  9. data/lib/rage/controller/api.rb +10 -34
  10. data/lib/rage/cookies.rb +1 -1
  11. data/lib/rage/deferred/deferred.rb +7 -0
  12. data/lib/rage/deferred/metadata.rb +8 -0
  13. data/lib/rage/deferred/scheduler.rb +25 -0
  14. data/lib/rage/deferred/task.rb +19 -5
  15. data/lib/rage/errors.rb +83 -0
  16. data/lib/rage/events/subscriber.rb +6 -1
  17. data/lib/rage/fiber.rb +14 -23
  18. data/lib/rage/fiber_scheduler.rb +51 -6
  19. data/lib/rage/internal.rb +15 -6
  20. data/lib/rage/middleware/fiber_wrapper.rb +1 -0
  21. data/lib/rage/openapi/builder.rb +1 -1
  22. data/lib/rage/openapi/converter.rb +5 -1
  23. data/lib/rage/openapi/nodes/method.rb +2 -1
  24. data/lib/rage/openapi/nodes/root.rb +2 -1
  25. data/lib/rage/openapi/openapi.rb +33 -1
  26. data/lib/rage/openapi/parser.rb +73 -2
  27. data/lib/rage/openapi/parsers/ext/alba.rb +30 -2
  28. data/lib/rage/openapi/parsers/ext/blueprinter.rb +110 -0
  29. data/lib/rage/openapi/parsers/request.rb +2 -2
  30. data/lib/rage/openapi/parsers/response.rb +3 -2
  31. data/lib/rage/openapi/parsers/yaml.rb +27 -5
  32. data/lib/rage/params_parser.rb +2 -2
  33. data/lib/rage/pubsub/adapters/redis.rb +2 -1
  34. data/lib/rage/router/constrainer.rb +1 -1
  35. data/lib/rage/router/dsl.rb +7 -2
  36. data/lib/rage/sse/application.rb +1 -0
  37. data/lib/rage/telemetry/tracer.rb +1 -0
  38. data/lib/rage/version.rb +1 -1
  39. data/lib/rage-rb.rb +6 -0
  40. metadata +6 -4
  41. data/lib/rage/cable/adapters/base.rb +0 -16
  42. data/lib/rage/cable/adapters/redis.rb +0 -128
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbc12eab5d13d5570107f5c7bf6d0b68f6f652eccc1eaafefdb47ef92ed3b264
4
- data.tar.gz: 7adca2c0e96d38ca69c32f4fc12b03984b6aa46578d48d084645e11891915e99
3
+ metadata.gz: 177c149ab02c459e231c8853a303f23da977767073611fa1996e3e9b4f3ebd7c
4
+ data.tar.gz: a28617f5ea6d85f95eb0d900582dddacf2fb83f8190f31e46f84700419ccfdc2
5
5
  SHA512:
6
- metadata.gz: cb010618b1afebee5baa5fee44963d8cdf4f1543769b79ee81d26e9a0815c3ea59d6b0cf4dad2f53adabbe8d26ae0688876d2a0f690e1497173469446ba4a9e8
7
- data.tar.gz: ee292c1c8b93fed32dbe015e6de898df1f0bb63ede69deb2b1eb41cddee1e339ade19ed8981682697721b02c3923bb8782358616c5d7c264cc0d465887b6e79b
6
+ metadata.gz: d1c422f7d8755b56a88203f5669d36d518a1ff69a9bc2fc9e8fbd9e0b628fc25884c44615bfbc7f577e3519e9be3c5499dc4cf7fd136065e6292e95fe1833635
7
+ data.tar.gz: 10a7692229b832f1eeb540114835c9ac49a317372e796163c3be7eb8d3d906cf89e9e3cd18c033e61834fa8dc192f1dbfc3a25f509f0a6c7496b0be8dcb4e026
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.25.0] - 2026-06-03
4
+
5
+ ### Added
6
+
7
+ - Implement `FiberScheduler#fiber_interrupt` (#283).
8
+ - [OpenAPI] Add Blueprinter parser scaffold and class detection (#287)
9
+ - [OpenAPI] Extend @response / @request tag syntax to accept serializer options (#299)
10
+ - Add `FiberScheduler#blocking_operation_wait` (#303).
11
+ - [OpenAPI] Added static parsing of basic Blueprinter fields (`identifier`, `field`, `fields`) for OpenAPI schema generation. (#289)
12
+
13
+ ### Fixed
14
+
15
+ - [API] Reject malformed or empty HTTP token authorization headers.
16
+ - [Cookies] Strip the port from `HTTP_HOST` before matching configured cookie domains.
17
+ - [Router] Strip the port from `HTTP_HOST` before matching exact host constraints.
18
+
19
+ ## [1.24.0] - 2026-05-12
20
+
21
+ ### Added
22
+
23
+ - [Deferred] Add tests for log context capture and backward-compatible restore by [@jsxs0](https://github.com/jsxs0) (#274).
24
+ - [OpenAPI] Add support for per-endpoint OAuth2/OpenID scopes via `@auth_scope` tag by [@Piyush-Goenka](https://github.com/Piyush-Goenka) (#272).
25
+ - Reuse `define_dynamic_method` and `define_maybe_yield` methods in `RageController::API` from `Rage::Internal` by [@numice](https://github.com/numice) (#273).
26
+ - Add the `form_actions` router configuration (#278).
27
+ - [Deferred] Add native periodic task scheduling with multi-process leader election via `File#flock` by [@Abishekcs](https://github.com/Abishekcs) (#233).
28
+ - [OpenAPI] Support optional attributes and `Array<>` syntax by [@ayushman1210](https://github.com/ayushman1210) (#228).
29
+ - [Errors] Add centralized error reporting interface via `Rage.errors` and `config.error_handlers` by [@Digvijay-x1](https://github.com/Digvijay-x1) (#275).
30
+
31
+ ### Fixed
32
+
33
+ - [OpenAPI] Fix SystemStackError in Alba parser with circular associations (#268).
34
+ - Rewind `rack.input` when parsing request body (#279).
35
+
36
+ ### Changed
37
+
38
+ - [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).
39
+ - Update `Rage::Cable` to use the new `PubSub` module (#281).
40
+
3
41
  ## [1.23.0] - 2026-04-15
4
42
 
5
43
  ### 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
@@ -6,9 +6,9 @@
6
6
  ![Tests](https://github.com/rage-rb/rage/actions/workflows/main.yml/badge.svg)
7
7
  ![Ruby Requirement](https://img.shields.io/badge/Ruby-3.3%2B-%23f40000)
8
8
 
9
- **Rage** is an API-first Ruby framework with a modern, fiber-based runtime that enables transparent, non-blocking concurrency while preserving familiar developer ergonomics. It focuses on **capability and operational simplicity**, letting teams build production-grade systems in a single, coherent runtime.
9
+ **Rage** is an API-first Ruby web framework that combines the developer experience of Rails with fiber-based concurrency. You write standard synchronous Ruby code - Rage handles the concurrency, running APIs, background jobs, and WebSockets in a single process with fewer moving parts.
10
10
 
11
- Rage uses Rails compatibility as a foundation and provides backend primitives optimized for a single-runtime model: background jobs that run in-process, WebSockets without external dependencies, object-oriented domain events, and automatic API documentation.
11
+ Rage uses Rails compatibility as a foundation and provides backend primitives optimized for a single-runtime model: background jobs that run in-process, scalable WebSockets and SSE streams, object-oriented domain events, and automatic API documentation.
12
12
 
13
13
  ## Why Rage
14
14
 
@@ -24,22 +24,73 @@ In the Ruby ecosystem, these concerns typically mean more infrastructure: Redis,
24
24
 
25
25
  Rage takes a different approach: **collapse backend concerns into a single runtime** by embracing Ruby's fiber-based concurrency model. This reduces operational complexity while keeping familiar Ruby ergonomics.
26
26
 
27
+ ## Key Capabilities
28
+
29
+ - **Rails Compatibility** - Familiar controller API, routing DSL, and conventions. Migrate gradually or start fresh.
30
+ - **True Concurrency** - Fiber-based architecture handles I/O without threads, locks, or async/await syntax. Your code looks synchronous but runs concurrently.
31
+ - **Zero-dependency WebSockets** - Action Cable-compatible real-time features that work out-of-the-box, with built-in IPC for multi-process deployments.
32
+ - **Server-Sent Events** - Native SSE streaming with no external dependencies. Built for live feeds, progress updates, and LLM response streaming.
33
+ - **Auto-generated OpenAPI** - Documentation generated from your controllers using simple comment tags.
34
+ - **In-process Background Jobs** - A durable, persistent queue that runs inside your app process. No external dependencies or separate worker processes required.
35
+ - **Built-in Observability** - Track and measure application behavior with `Rage::Telemetry`. Integrate with external monitoring platforms or build custom observability solutions.
36
+
37
+ ## Installation
38
+
39
+ Install the gem:
40
+
41
+ ```
42
+ $ gem install rage-rb
43
+ ```
44
+
45
+ Create a new app:
46
+
47
+ ```
48
+ $ rage new my_app
49
+ ```
50
+
51
+ Switch to your new application and install dependencies:
52
+
53
+ ```
54
+ $ cd my_app
55
+ $ bundle install
56
+ ```
57
+
58
+ (Optional 🤖) Install agent skills:
59
+
60
+ ```
61
+ $ rage skills install
62
+ ```
63
+
64
+ Start up the server and visit http://localhost:3000.
65
+
66
+ ```
67
+ $ rage s
68
+ ```
69
+
70
+ Start coding!
71
+
72
+ ## How It Works
73
+
74
+ Rage runs each request in a separate fiber. When your code performs I/O operations - HTTP requests, database queries, file reads - the fiber automatically pauses, and Rage processes other requests. When the I/O completes, the fiber resumes exactly where it left off.
75
+
76
+ This happens automatically. You write standard Ruby code, and Rage handles the concurrency.
77
+
27
78
  ### Unified Runtime in Action
28
79
 
29
80
  Here's what single runtime looks like in practice:
30
81
 
31
82
  ```ruby
32
83
  class OrdersController < RageController::API
84
+ # Create an order record.
85
+ # @request { amount: Float, product_id: Integer }
86
+ # @response 201 Order
33
87
  def create
34
88
  order = Order.create!(order_params)
35
89
 
36
90
  # Schedule background job - runs in-process, no Redis needed
37
91
  SendOrderConfirmation.enqueue(order.id)
38
92
 
39
- # Publish domain event - subscribers execute immediately or async
40
- Rage::Events.publish(OrderPlaced.new(order: order))
41
-
42
- # Broadcast to WebSocket subscribers - no Action Cable/Redis needed
93
+ # Broadcast to WebSocket subscribers - built in, no external services needed
43
94
  Rage::Cable.broadcast("orders", { status: "created", order_id: order.id })
44
95
 
45
96
  render json: order, status: :created
@@ -55,22 +106,15 @@ class SendOrderConfirmation
55
106
  OrderMailer.confirmation(order).deliver
56
107
  end
57
108
  end
109
+ ```
58
110
 
59
- # Domain event - typed, object-oriented
60
- OrderPlaced = Data.define(:order)
111
+ This all runs in a single process. No external queues, no separate worker dynos, no additional infrastructure.
61
112
 
62
- # Event subscriber
63
- class UpdateInventory
64
- include Rage::Events::Subscriber
65
- subscribe_to OrderPlaced
113
+ ## Two Ways to Use Rage
66
114
 
67
- def call(event)
68
- Inventory.decrement(event.order.items.length)
69
- end
70
- end
71
- ```
115
+ **Standalone**: Create new services with `rage new`. You get a clean project structure, CLI tools, and everything needed to build production-ready APIs from scratch.
72
116
 
73
- This all runs in a single process. No external queues, no separate worker dynos, no Redis for pub/sub.
117
+ **Rails Integration**: Add Rage to existing Rails applications for gradual migration. Use Rage for new endpoints or high-traffic routes while keeping the rest of your Rails app unchanged. See the [Rails Integration Guide](https://rage-rb.dev/docs/rails) for details.
74
118
 
75
119
  ## Coming from Rails?
76
120
 
@@ -83,7 +127,7 @@ You write familiar synchronous Ruby code. Rage handles the concurrency.
83
127
  **What changes:**
84
128
 
85
129
  - One deployment unit instead of API servers + worker processes
86
- - No Redis required for jobs or broadcasts
130
+ - No external dependencies for jobs or broadcasts
87
131
  - Domain events as objects, not string-based notifications
88
132
  - OpenAPI specs generated automatically from your code
89
133
 
@@ -95,117 +139,16 @@ You write familiar synchronous Ruby code. Rage handles the concurrency.
95
139
 
96
140
  Think of Rage as Rails ergonomics with a runtime designed for modern API systems, where operational simplicity is a first-class concern.
97
141
 
98
- ## Core Ideas
99
-
100
- ### 1. Unified Backend Runtime
101
-
102
- Rage runs HTTP APIs, background jobs, and WebSockets in the same process by default:
103
-
104
- - No separate worker processes
105
- - No Redis required for jobs or broadcasts
106
- - One deployment unit for most applications
107
-
108
- This simplifies both local development and production setup.
109
-
110
- For high-scale scenarios, Rage supports multi-process deployments and allows Rage processes to communicate directly when needed.
111
-
112
- ### 2. API-First, Rails-Compatible
113
-
114
- Rage provides a familiar Rails-like programming model with API-focused improvements:
115
-
116
- - Controllers and routing that feel like Rails
117
- - Active Record compatibility
118
- - Incremental adoption for existing Rails applications
119
- - OpenAPI specs auto-generated from your code
120
-
121
- Rails compatibility is the foundation. Rage builds on top of that foundation with new primitives designed for high-concurrency, API-first architectures.
122
-
123
- ### 3. Built-in Asynchronous Execution
124
-
125
- Rage ships with **fiber-based, in-process background jobs**:
126
-
127
- - Zero setup - no Redis, no configuration
128
- - Jobs persist across restarts
129
- - Scheduled and executed within the same runtime using fibers for concurrency
130
-
131
- For teams that need distributed job processing, Rage works with existing solutions. But most applications can start simple and stay simple.
132
-
133
- ### 4. Structured Domain Events
134
-
135
- Rage includes a built‑in event bus designed for **object‑oriented domain events**:
136
-
137
- * Events are classes with explicit attributes, not hashes or strings
138
- * Subscribers listen to event classes or mixins
139
- * Type-safe and refactorable
140
-
141
- This encourages clear domain modeling and avoids the brittleness of string-based notification systems.
142
-
143
- ### 5. Observability by Design
144
-
145
- Observability is not an afterthought:
146
-
147
- - Structured logging by default
148
- - Dedicated observability interface for HTTP, background, and real-time features
149
- - OpenAPI specifications generated automatically from the running application
150
-
151
- API contracts stay in sync with code by default - no separate documentation pipelines.
152
-
153
- ### 6. Performance That Enables Simplicity
154
-
155
- Rage's fiber-based concurrency delivers strong performance for I/O-heavy workloads, but performance is a means to an end: operational simplicity.
156
-
157
- By handling concurrency efficiently, Rage lets you:
158
-
159
- - Run fewer servers
160
- - Deploy fewer services
161
- - Reduce infrastructure by handling workloads that traditionally required separate microservices
162
-
163
- The goal is to let teams write maintainable Ruby code while natively handling massive concurrency, eliminating the need for premature optimization or infrastructure sprawl.
164
-
165
- ## Philosophy
166
-
167
- Rage is intentionally conservative about change.
168
-
169
- The framework prioritizes:
170
-
171
- - **Stable public APIs**
172
- - **Long deprecation cycles**
173
- - **Minimal external dependencies**
174
-
175
- The goal is to let teams build systems that **age well** - without constant rewrites or growing infrastructure complexity.
176
-
177
- Our aspiration: the task "Upgrade Rage" never appears in your ticketing system. Most updates should be as simple as `bundle update`.
178
-
179
- ## What Rage Is (and Isn't)
180
-
181
- **Rage is:**
182
-
183
- - Focused on backend APIs
184
- - Opinionated about operational simplicity
185
- - Designed for long-term stability
186
- - Rails-compatible but architecturally independent
187
-
188
- **Rage is not:**
189
-
190
- - A full-stack framework - no view layer, no asset pipeline
191
- - A Rails clone - compatibility is a bridge, not the destination
192
- - Trying to do everything - deliberate scope boundaries
193
-
194
- ## Who Rage Is For
195
-
196
- Rage is a good fit if you:
142
+ ## Stability
197
143
 
198
- - Build API-only backends in Ruby
199
- - Care about operational simplicity over maximum flexibility
200
- - Want fewer moving parts in production
201
- - Prefer explicit, object-oriented design
202
- - Value long-term stability over cutting-edge features
144
+ Rage prioritizes stable public APIs, long deprecation cycles, and minimal external dependencies. Our aspiration: the task "Upgrade Rage" never appears in your ticketing system. Most updates should be as simple as `bundle update`.
203
145
 
204
146
  ## Learn More
205
147
 
206
148
  - Documentation: [https://rage-rb.dev](https://rage-rb.dev/docs/intro)
207
149
  - API Reference: [https://rage-rb.dev/api](https://rage-rb.dev/api)
208
150
  - Architecture: [ARCHITECTURE.md](https://github.com/rage-rb/rage/blob/main/ARCHITECTURE.md)
151
+ - Contributing: [CONTRIBUTING.md](https://github.com/rage-rb/rage/blob/main/CONTRIBUTING.md)
209
152
 
210
153
  Contributions and thoughtful feedback are welcome.
211
154
 
@@ -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