rage-rb 1.20.1 → 1.21.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: 8aa7e9daf1be975a84d7d1106d80add30bdfd77e1419b00f7482d8bf426f5e86
4
- data.tar.gz: 58a2c72cc2add61f0c2d288a82806191241d8f09ce18657e09f4606c537acd49
3
+ metadata.gz: 7120d5b1b4f9ae8307d199389fe7efbffff3a950d3d312b80c954a00b0c0572e
4
+ data.tar.gz: d3ce27d3cfa545ac38e7b3a7091c24bdd394ab954860da1418a97a16c65ef482
5
5
  SHA512:
6
- metadata.gz: 07db2cb1fb418630c4ce99121a88c010e84893c0d6b52a8dd7a6251b99a56fd1666b42b0269d91b8686779fd76c3ee9df72965d82484dcacf16cd3fc4cf787a9
7
- data.tar.gz: a078573af7ccf125145ba63514c291436b34552862384073062868f19ae08326b06bc1bf9ade39c75d53f8ab024df8a4d1a9041a29ca5c4c3b8e51846905e137
6
+ metadata.gz: aeb7938480349b63ddf30525b7bf89b6c214cf12e1b859311f6c8c7976f4c20866897fbb8cbaefa271fcf65234905b97d0428a1b6f85fb307b91f415abc7538c
7
+ data.tar.gz: 78b9c08dc3769fcec16ec011699d06246e7177d0459e0e4a39a592e5f62386b5142da133337a53eccb2f1c9175a3986a064e0bc16395708901de2c61080acede
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.21.0] - 2026-02-25
4
+
5
+ ### Added
6
+
7
+ - [Cable] Add RSpec test helpers (#210).
8
+ - [Cable] Add support for `stream_for`/`broadcast_to` (#207).
9
+ - Add `skills` CLI (#218).
10
+ - Support inline context for `Rage::Logger` (#206).
11
+
12
+ ### Fixed
13
+
14
+ - Ensure correct log context isolation for intersecting fibers (#205).
15
+ - Ensure Cable middleware don't duplicate when mounted in routes (#202).
16
+ - Documentation updates by [@cuneyter](https://github.com/cuneyter) (#200).
17
+ - Rely on `Rack::Session` for mounted apps (#201).
18
+
19
+ ### Fixed
20
+
3
21
  ## [1.20.1] - 2026-02-10
4
22
 
5
23
  ### Fixed
data/README.md CHANGED
@@ -6,190 +6,208 @@
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.2%2B-%23f40000)
8
8
 
9
- Rage is a high-performance Ruby web framework that combines the developer experience of Rails with the scalability of fiber-based concurrency. Designed for API-first applications, it allows you to handle massive traffic loads using standard synchronous Ruby code - no complex async/await syntax required.
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.
10
10
 
11
- If you love Rails but need better performance for I/O-heavy workloads, Rage provides the perfect balance: familiar conventions, low overhead, and a commitment to stability.
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.
12
12
 
13
- ## Why Rage?
13
+ ## Why Rage
14
14
 
15
- Building high-performance APIs in Ruby shouldn't mean abandoning the conventions you know. Rage gives you Rails-like controllers, routing, and patterns, but runs on **fiber-based concurrency** that makes your application naturally non-blocking. When your code waits on database queries, HTTP calls, or other I/O, Rage automatically handles thousands of other requests instead of sitting idle.
15
+ Modern backends are more than request/response cycles. They require:
16
16
 
17
- Rage was built to solve the performance and stability gaps that often drive teams to migrate away from Ruby, providing a modern engine that keeps the ecosystem competitive.
17
+ * Asynchronous execution
18
+ * Background jobs
19
+ * Real-time communication
20
+ * Observability and telemetry
21
+ * Clear domain boundaries
18
22
 
19
- **Key capabilities:**
23
+ In the Ruby ecosystem, these concerns typically mean more infrastructure: Redis, Sidekiq, separate worker processes, custom logging solutions, and multiple deployment units.
20
24
 
21
- - **Rails compatibility** - Familiar controller API, routing DSL, and conventions. Migrate gradually or start fresh.
22
- - **True concurrency** - Fiber-based architecture handles I/O without threads, locks, or async/await syntax. Your code looks synchronous but runs concurrently.
23
- - **Zero-dependency WebSockets** - Action Cable-compatible real-time features that work out-of-the-box without Redis, even in multi-process mode.
24
- - **Auto-generated OpenAPI** - Documentation generated from your controllers using simple comment tags.
25
- - **In-process Background jobs** - A durable, persistent queue that runs inside your app process. No Redis or separate worker processes required.
26
- - **Built-in Observability** - Track and measure application behavior with `Rage::Telemetry`. Integrate with external monitoring platforms or build custom observability solutions.
27
- - **Stable and focused** - Our goal is that the task "Upgrade Rage" never appears in your ticketing system. We focus strictly on APIs, maintain long-term deprecation cycles, and ensure that most updates are as simple as a `bundle update`.
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.
28
26
 
29
- Rage is API-only by design. Modern applications benefit from clear separation between backend and frontend, and Rage focuses exclusively on doing APIs well.
27
+ ### Unified Runtime in Action
30
28
 
31
- ## Installation
29
+ Here's what single runtime looks like in practice:
32
30
 
33
- Install the gem:
31
+ ```ruby
32
+ class OrdersController < RageController::API
33
+ def create
34
+ order = Order.create!(order_params)
34
35
 
35
- ```
36
- $ gem install rage-rb
37
- ```
36
+ # Schedule background job - runs in-process, no Redis needed
37
+ SendOrderConfirmation.enqueue(order.id)
38
38
 
39
- Create a new app:
39
+ # Publish domain event - subscribers execute immediately or async
40
+ Rage::Events.publish(OrderPlaced.new(order: order))
40
41
 
41
- ```
42
- $ rage new my_app
43
- ```
42
+ # Broadcast to WebSocket subscribers - no Action Cable/Redis needed
43
+ Rage::Cable.broadcast("orders", { status: "created", order_id: order.id })
44
44
 
45
- Switch to your new application and install dependencies:
45
+ render json: order, status: :created
46
+ end
47
+ end
46
48
 
47
- ```
48
- $ cd my_app
49
- $ bundle
50
- ```
49
+ # Background job - runs in the same process, persisted to disk
50
+ class SendOrderConfirmation
51
+ include Rage::Deferred::Task
52
+
53
+ def perform(order_id)
54
+ order = Order.find(order_id)
55
+ OrderMailer.confirmation(order).deliver
56
+ end
57
+ end
51
58
 
52
- Start up the server and visit http://localhost:3000.
59
+ # Domain event - typed, object-oriented
60
+ OrderPlaced = Data.define(:order)
53
61
 
54
- ```
55
- $ rage s
62
+ # Event subscriber
63
+ class UpdateInventory
64
+ include Rage::Events::Subscriber
65
+ subscribe_to OrderPlaced
66
+
67
+ def call(event)
68
+ Inventory.decrement(event.order.items.length)
69
+ end
70
+ end
56
71
  ```
57
72
 
58
- Start coding!
73
+ This all runs in a single process. No external queues, no separate worker dynos, no Redis for pub/sub.
59
74
 
60
- ## How It Works
75
+ ## Coming from Rails?
61
76
 
62
- 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.
77
+ Rage keeps the parts of Rails that work - controllers, routing, Active Record compatibility, and conventions - but rethinks how backend systems are run.
63
78
 
64
- This happens transparently. You write normal Ruby code, and Rage handles the concurrency.
79
+ Instead of adding separate job queues, Redis, and multiple deployment units as your app grows, Rage uses Ruby's fiber-based concurrency to run APIs, background jobs, WebSockets, and domain events **in the same process**.
65
80
 
66
- ### Example
81
+ You write familiar synchronous Ruby code. Rage handles the concurrency.
67
82
 
68
- Here's a controller that fetches data from an external API:
83
+ **What changes:**
69
84
 
70
- ```ruby
71
- require "net/http"
85
+ - One deployment unit instead of API servers + worker processes
86
+ - No Redis required for jobs or broadcasts
87
+ - Domain events as objects, not string-based notifications
88
+ - OpenAPI specs generated automatically from your code
72
89
 
73
- class PagesController < RageController::API
74
- rescue_from SocketError do |_|
75
- render json: { message: "error" }, status: 500
76
- end
90
+ **What stays the same:**
77
91
 
78
- before_action :set_metadata
92
+ - Controller conventions and routing DSL
93
+ - Active Record integration
94
+ - Incremental adoption for existing Rails apps
79
95
 
80
- def show
81
- page = Net::HTTP.get(URI("https://httpbin.org/json"))
82
- render json: { page: page, metadata: @metadata }
83
- end
96
+ Think of Rage as Rails ergonomics with a runtime designed for modern API systems, where operational simplicity is a first-class concern.
84
97
 
85
- private
98
+ ## Core Ideas
86
99
 
87
- def set_metadata
88
- @metadata = { format: "json", time: Time.now.to_i }
89
- end
90
- end
91
- ```
100
+ ### 1. Unified Backend Runtime
92
101
 
93
- This looks like a standard Rails controller, and it is - except during `Net::HTTP.get`, Rage automatically pauses this fiber and processes other requests. When the HTTP call completes, Rage resumes exactly where it left off. This happens automatically for HTTP requests, PostgreSQL, MySQL, and other I/O operations.
102
+ Rage runs HTTP APIs, background jobs, and WebSockets in the same process by default:
94
103
 
95
- The routes are equally familiar:
104
+ - No separate worker processes
105
+ - No Redis required for jobs or broadcasts
106
+ - One deployment unit for most applications
96
107
 
97
- ```ruby
98
- Rage.routes.draw do
99
- get "page", to: "pages#show"
100
- end
101
- ```
108
+ This simplifies both local development and production setup.
102
109
 
103
- ### Parallel Execution
110
+ For high-scale scenarios, Rage supports multi-process deployments and allows Rage processes to communicate directly when needed.
104
111
 
105
- Need to make multiple I/O calls? Use `Fiber.await` to run them concurrently:
112
+ ### 2. API-First, Rails-Compatible
106
113
 
107
- ```ruby
108
- require "net/http"
114
+ Rage provides a familiar Rails-like programming model with API-focused improvements:
109
115
 
110
- class PagesController < RageController::API
111
- def index
112
- pages = Fiber.await([
113
- Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/json")) },
114
- Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/html")) },
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
116
120
 
117
- render json: { pages: pages }
118
- end
119
- end
120
- ```
121
+ Rails compatibility is the foundation. Rage builds on top of that foundation with new primitives designed for high-concurrency, API-first architectures.
121
122
 
122
- Instead of waiting for each request sequentially, Rage executes them concurrently and waits for all to complete.
123
+ ### 3. Built-in Asynchronous Execution
123
124
 
124
- ## Two Ways to Use Rage
125
+ Rage ships with **fiber-based, in-process background jobs**:
125
126
 
126
- **Standalone**: Create new services with `rage new`. You get a clean project structure, CLI tools, and everything needed to build high-performance APIs from scratch.
127
+ - Zero setup - no Redis, no configuration
128
+ - Jobs persist across restarts
129
+ - Scheduled and executed within the same runtime using fibers for concurrency
127
130
 
128
- **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.
131
+ For teams that need distributed job processing, Rage works with existing solutions. But most applications can start simple and stay simple.
129
132
 
130
- ## Documentation
133
+ ### 4. Structured Domain Events
131
134
 
132
- - [Getting Started](https://rage-rb.dev/docs/intro/) - Core concepts and setup
133
- - [Controllers](https://rage-rb.dev/docs/controllers/) - Request handling and callbacks
134
- - [Routing](https://rage-rb.dev/docs/routing/) - RESTful routes and namespaces
135
- - [WebSockets](https://rage-rb.dev/docs/websockets/) - Real-time communication
136
- - [OpenAPI](https://rage-rb.dev/docs/openapi/) - Auto-generated documentation
137
- - [Background Jobs](https://rage-rb.dev/docs/deferred/) - In-process queue system
138
- - [API Reference](https://rage-rb.dev/api/) - Detailed API documentation
135
+ Rage includes a built‑in event bus designed for **object‑oriented domain events**:
139
136
 
140
- For contributors, check the [architecture doc](https://github.com/rage-rb/rage/blob/main/ARCHITECTURE.md) to understand how Rage's components work together.
137
+ * Events are classes with explicit attributes, not hashes or strings
138
+ * Subscribers listen to event classes or mixins
139
+ * Type-safe and refactorable
141
140
 
142
- ## Performance
141
+ This encourages clear domain modeling and avoids the brittleness of string-based notification systems.
143
142
 
144
- Rage's fiber-based architecture delivers high throughput with minimal overhead. By stripping away the "framework tax", Rage gives your team more leeway to write slow-but-maintainable Ruby code without compromising the end-user experience.
143
+ ### 5. Observability by Design
145
144
 
146
- #### Simple JSON responses
145
+ Observability is not an afterthought:
147
146
 
148
- ```ruby
149
- class BenchmarksController < ApplicationController
150
- def index
151
- render json: { hello: "world" }
152
- end
153
- end
154
- ```
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
155
150
 
156
- ![Requests per second](https://github.com/user-attachments/assets/7bb783f8-5d1b-4e7d-b14d-dafe370d1acc)
151
+ API contracts stay in sync with code by default - no separate documentation pipelines.
157
152
 
153
+ ### 6. Performance That Enables Simplicity
158
154
 
159
- #### I/O-bound operations
155
+ Rage's fiber-based concurrency delivers strong performance for I/O-heavy workloads, but performance is a means to an end: operational simplicity.
160
156
 
161
- ```ruby
162
- class BenchmarksController < ApplicationController
163
- def index
164
- Net::HTTP.get(URI("<endpoint-that-responds-in-one-second>"))
165
- head :ok
166
- end
167
- end
168
- ```
157
+ By handling concurrency efficiently, Rage lets you:
169
158
 
170
- ![Time to complete 100 requests](https://github.com/user-attachments/assets/5155a65f-2f11-4303-b5e4-a74d3d123c16)
159
+ - Run fewer servers
160
+ - Deploy fewer services
161
+ - Reduce infrastructure by handling workloads that traditionally required separate microservices
171
162
 
172
- #### Database Queries
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.
173
164
 
174
- ```ruby
175
- class BenchmarksController < ApplicationController
176
- def show
177
- render json: World.find(rand(1..10_000))
178
- end
179
- end
180
- ```
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
181
193
 
182
- ![Requests per second-2](https://github.com/user-attachments/assets/06f64a08-316f-4b24-ba2d-39ac395366aa)
194
+ ## Who Rage Is For
183
195
 
184
- ## Development
196
+ Rage is a good fit if you:
185
197
 
186
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
187
203
 
188
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
204
+ ## Learn More
189
205
 
190
- ## Contributing
206
+ - Documentation: [https://rage-rb.dev](https://rage-rb.dev/docs/intro)
207
+ - API Reference: [https://rage-rb.dev/api](https://rage-rb.dev/api)
208
+ - Architecture: [ARCHITECTURE.md](https://github.com/rage-rb/rage/blob/main/ARCHITECTURE.md)
191
209
 
192
- Bug reports and pull requests are welcome on GitHub at https://github.com/rage-rb/rage. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/rage-rb/rage/blob/main/CODE_OF_CONDUCT.md).
210
+ Contributions and thoughtful feedback are welcome.
193
211
 
194
212
  ## License
195
213
 
data/lib/rage/all.rb CHANGED
@@ -36,7 +36,3 @@ require_relative "middleware/request_id"
36
36
  require_relative "middleware/body_finalizer"
37
37
 
38
38
  require_relative "telemetry/telemetry"
39
-
40
- if defined?(Sidekiq)
41
- require_relative "sidekiq_session"
42
- end
@@ -53,7 +53,11 @@ module Rage::Cable
53
53
  end
54
54
  end
55
55
 
56
- Rage.with_middlewares(application, Rage.config.cable.middlewares)
56
+ chain = Rage.with_middlewares(application, Rage.config.cable.middlewares)
57
+ application.define_singleton_method(:__rage_app_name) { "Rage::Cable" }
58
+ chain.define_singleton_method(:__rage_root_app) { application }
59
+
60
+ chain
57
61
  end
58
62
 
59
63
  # @private
@@ -289,6 +289,32 @@ class Rage::Cable::Channel
289
289
  @__periodic_timers << [callback, every]
290
290
  end
291
291
 
292
+ # Broadcast data to all the clients subscribed to a channel-local stream.
293
+ #
294
+ # @param streamable [#id, String, Symbol, Numeric, Array] an object that will be used to generate the stream name
295
+ # @param data [Object] the data to send to the clients
296
+ # @raise [ArgumentError] if the streamable object does not satisfy the type requirements
297
+ # @example
298
+ # NotificationsChannel.broadcast_to(current_user, { message: "You have a new notification!" })
299
+ def broadcast_to(streamable, data)
300
+ Rage.cable.broadcast(__stream_name_for(streamable), data)
301
+ end
302
+
303
+ # @private
304
+ def __stream_name_for(streamables)
305
+ stream_name = Array(streamables).map do |streamable|
306
+ if streamable.respond_to?(:id)
307
+ "#{streamable.class.name}:#{streamable.id}"
308
+ elsif streamable.is_a?(String) || streamable.is_a?(Symbol) || streamable.is_a?(Numeric)
309
+ streamable
310
+ else
311
+ raise ArgumentError, "Unable to generate stream name. Expected an object that responds to `id`, got: #{streamable.class}"
312
+ end
313
+ end
314
+
315
+ "#{name}:#{stream_name.join(":")}"
316
+ end
317
+
292
318
  protected
293
319
 
294
320
  def set_up_periodic_timers
@@ -407,13 +433,39 @@ class Rage::Cable::Channel
407
433
  !!@__subscription_rejected
408
434
  end
409
435
 
410
- # Subscribe to a stream.
436
+ # Subscribe to a stream global stream. Global streams are not associated with any specific channel instance and can be used to broadcast data to multiple channels at once.
411
437
  #
412
438
  # @param stream [String] the name of the stream
439
+ # @raise [ArgumentError] if the stream name is not a String
440
+ # @example Subscribe to a stream
441
+ # class NotificationsChannel < Rage::Cable::Channel
442
+ # def subscribed
443
+ # stream_from "notifications"
444
+ # end
445
+ # end
446
+ # @example Broadcast to the stream
447
+ # Rage::Cable.broadcast("notifications", { message: "A new member has joined!" })
413
448
  def stream_from(stream)
449
+ raise ArgumentError, "Stream name must be a String" unless stream.is_a?(String)
414
450
  Rage.cable.__protocol.subscribe(@__connection, stream, @__params)
415
451
  end
416
452
 
453
+ # Subscribe to a local stream. Local streams are associated with a specific channel instance and can be used to send data to the current channel only.
454
+ #
455
+ # @param streamable [#id, String, Symbol, Numeric, Array] an object that will be used to generate the stream name
456
+ # @raise [ArgumentError] if the streamable object does not satisfy the type requirements
457
+ # @example Subscribe to a stream
458
+ # class NotificationsChannel < Rage::Cable::Channel
459
+ # def subscribed
460
+ # stream_for current_user
461
+ # end
462
+ # end
463
+ # @example Broadcast to the stream
464
+ # NotificationsChannel.broadcast_to(current_user, { message: "You have a new notification!" })
465
+ def stream_for(streamable)
466
+ stream_from(self.class.__stream_name_for(streamable))
467
+ end
468
+
417
469
  # Broadcast data to all the clients subscribed to a stream.
418
470
  #
419
471
  # @param stream [String] the name of the stream