rage-rb 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/OVERVIEW.md +32 -2
- data/README.md +1 -1
- data/lib/rage/all.rb +1 -0
- data/lib/rage/cable/adapters/redis.rb +1 -1
- data/lib/rage/cable/cable.rb +7 -2
- data/lib/rage/cable/channel.rb +4 -4
- data/lib/rage/cable/{protocol → protocols}/actioncable_v1_json.rb +30 -60
- data/lib/rage/cable/protocols/base.rb +88 -0
- data/lib/rage/cable/protocols/raw_web_socket_json.rb +144 -0
- data/lib/rage/configuration.rb +36 -3
- data/lib/rage/hooks.rb +25 -0
- data/lib/rage/openapi/parsers/ext/alba.rb +23 -9
- data/lib/rage/request.rb +0 -1
- data/lib/rage/router/constrainer.rb +0 -2
- data/lib/rage/router/node.rb +0 -2
- data/lib/rage/rspec.rb +1 -1
- data/lib/rage/setup.rb +3 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +4 -0
- data/rage.gemspec +1 -1
- metadata +8 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf740bcbb20d075fa39c87bf6b97b10daf6f2a92daf6bcf7a12a03f04dbf5621
|
4
|
+
data.tar.gz: 7fd73455dfd6cb646d52d2e6f798b558e62ffe343d3156901a867af53179e973
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d74c740eec61265329a2f7623e3fa053d1d3bb07c3da7162b7551beded19bb3f6dc855cf5f92ec8acc501446ab72ee8d147904794f8892d967ceba939b3a10e
|
7
|
+
data.tar.gz: 169cab9535c8dc1ec9d99614dc8201e3cc2215126669965809618ae2effdd9384a6a4ab121913ce149d299111fce7f39eef8a00a2ba362abee755514f5b5c099
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.16.0] - 2025-05-20
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- [Cable] Add the `RawJSON` protocol (#150).
|
8
|
+
- Add the `after_initialize` hook by [@serhii-sadovskyi](https://github.com/serhii-sadovskyi) (#149).
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
- Correctly parse plaintext responses in RSpec (#151).
|
13
|
+
- [OpenAPI] Correctly handle `root_key!` (#148).
|
14
|
+
- [OpenAPI] Correctly handle the `key` option in associations (#147).
|
15
|
+
|
16
|
+
## [1.15.1] - 2025-04-17
|
17
|
+
|
18
|
+
### Fixed
|
19
|
+
|
20
|
+
- [Cable] Create a subscription only when the reactor is started (#146).
|
21
|
+
|
3
22
|
## [1.15.0] - 2025-04-02
|
4
23
|
|
5
24
|
### Added
|
data/OVERVIEW.md
CHANGED
@@ -1,10 +1,18 @@
|
|
1
|
-
###
|
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
|
2
10
|
|
3
11
|
The following diagram describes some of Rage's internal components and the way they interact with each other:
|
4
12
|
|
5
13
|

|
6
14
|
|
7
|
-
### Executing
|
15
|
+
### Executing Controller Actions
|
8
16
|
|
9
17
|
When `Rage::Router::DSL` parses the `config/routes.rb` file and calls the `Rage::Router::Backend` class, it registers actions and stores handler procs.
|
10
18
|
|
@@ -51,3 +59,25 @@ After that, Rage will create and store a handler proc that will look exactly lik
|
|
51
59
|
```
|
52
60
|
|
53
61
|
All of this happens at boot time. Once the request comes in at runtime, Rage will only need to retrieve the handler proc defined earlier and call it.
|
62
|
+
|
63
|
+
### Cable Workflow
|
64
|
+
|
65
|
+
The following diagram describes the components of a `Rage::Cable` application:
|
66
|
+
|
67
|
+

|
68
|
+
|
69
|
+
### OpenAPI Workflow
|
70
|
+
|
71
|
+
The following diagram describes the flow of `Rage::OpenAPI`:
|
72
|
+
|
73
|
+
<img width="800" src="https://github.com/user-attachments/assets/b4a87b1e-9a0f-4432-a3e9-0106ff546f3f" />
|
74
|
+
|
75
|
+
### Design Principles
|
76
|
+
|
77
|
+
* **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.
|
78
|
+
|
79
|
+
* **Performance Over Code Style:** we recognize the distinct requirements of framework and client code. Testability, readability, and maintainability are crucial for client code used in application development. Conversely, library code addresses different tasks and should be designed with different objectives. In library code, performance and abstraction to enable future modifications while maintaining backward compatibility take precedence over typical client code concerns, though testability and readability remain important.
|
80
|
+
|
81
|
+
* **Rails Compatibility:** Rails compatibility is a key objective to ensure a seamless transition for developers. While it may not be feasible to replicate every method implemented in Rails, the framework should function in a familiar and expected manner.
|
82
|
+
|
83
|
+
* **Single-Threaded Fiber-Based Approach:** each request is processed in a separate, isolated execution context (Fiber), pausing whenever it encounters blocking I/O. This single-threaded approach eliminates thread synchronization overhead, leading to enhanced performance and simplified code.
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
[](https://badge.fury.io/rb/rage-rb)
|
6
6
|

|
7
|
-

|
8
8
|
|
9
9
|
Rage is a high-performance framework compatible with Rails, featuring [WebSocket](https://github.com/rage-rb/rage/wiki/WebSockets-guide) support and automatic generation of [OpenAPI](https://github.com/rage-rb/rage/wiki/OpenAPI-Guide) documentation for your APIs. The framework is built on top of [Iodine](https://github.com/rage-rb/iodine) and is based on the following design principles:
|
10
10
|
|
data/lib/rage/all.rb
CHANGED
@@ -107,7 +107,7 @@ class Rage::Cable::Adapters::Redis < Rage::Cable::Adapters::Base
|
|
107
107
|
if data
|
108
108
|
data[@redis_stream].each do |id, (_, stream_name, _, serialized_data, _, broadcaster_uuid, _, message_uuid)|
|
109
109
|
if broadcaster_uuid != @server_uuid && message_uuid != last_message_uuid
|
110
|
-
Rage.
|
110
|
+
Rage.cable.__protocol.broadcast(stream_name, JSON.parse(serialized_data))
|
111
111
|
end
|
112
112
|
|
113
113
|
last_id = id
|
data/lib/rage/cable/cable.rb
CHANGED
@@ -36,6 +36,7 @@ module Rage::Cable
|
|
36
36
|
@__protocol ||= Rage.config.cable.protocol.tap { |protocol| protocol.init(__router) }
|
37
37
|
end
|
38
38
|
|
39
|
+
# @private
|
39
40
|
def self.__adapter
|
40
41
|
@__adapter ||= Rage.config.cable.adapter
|
41
42
|
end
|
@@ -136,11 +137,15 @@ module Rage::Cable
|
|
136
137
|
autoload :Redis, "rage/cable/adapters/redis"
|
137
138
|
end
|
138
139
|
|
139
|
-
module
|
140
|
+
module Protocols
|
140
141
|
end
|
142
|
+
|
143
|
+
Protocol = Protocols
|
141
144
|
end
|
142
145
|
|
143
|
-
require_relative "
|
146
|
+
require_relative "protocols/base"
|
147
|
+
require_relative "protocols/actioncable_v1_json"
|
148
|
+
require_relative "protocols/raw_web_socket_json"
|
144
149
|
require_relative "channel"
|
145
150
|
require_relative "connection"
|
146
151
|
require_relative "router"
|
data/lib/rage/cable/channel.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "set"
|
2
|
-
|
3
1
|
class Rage::Cable::Channel
|
4
2
|
# @private
|
5
3
|
INTERNAL_ACTIONS = [:subscribed, :unsubscribed]
|
@@ -18,6 +16,8 @@ class Rage::Cable::Channel
|
|
18
16
|
public_instance_methods(true) - Rage::Cable::Channel.public_instance_methods(true)
|
19
17
|
).reject { |m| m.start_with?("__rage_tmp") || m.start_with?("__run") }
|
20
18
|
|
19
|
+
actions.reject! { |m| m != :receive } unless Rage.cable.__protocol.supports_rpc?
|
20
|
+
|
21
21
|
@__prepared_actions = (INTERNAL_ACTIONS + actions).each_with_object({}) do |action_name, memo|
|
22
22
|
memo[action_name] = __register_action_proc(action_name)
|
23
23
|
end
|
@@ -406,7 +406,7 @@ class Rage::Cable::Channel
|
|
406
406
|
#
|
407
407
|
# @param stream [String] the name of the stream
|
408
408
|
def stream_from(stream)
|
409
|
-
Rage.
|
409
|
+
Rage.cable.__protocol.subscribe(@__connection, stream, @__params)
|
410
410
|
end
|
411
411
|
|
412
412
|
# Broadcast data to all the clients subscribed to a stream.
|
@@ -429,7 +429,7 @@ class Rage::Cable::Channel
|
|
429
429
|
# transmit({ message: "Hello!" })
|
430
430
|
# end
|
431
431
|
def transmit(data)
|
432
|
-
message = Rage.
|
432
|
+
message = Rage.cable.__protocol.serialize(@__params, data)
|
433
433
|
|
434
434
|
if @__is_subscribing
|
435
435
|
# we expect a confirmation message to be sent as a result of a successful subscribe call;
|
@@ -1,29 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "zlib"
|
4
|
-
require "set"
|
5
|
-
|
6
3
|
##
|
7
|
-
#
|
8
|
-
#
|
4
|
+
# This is an implementation of the `Action Cable` protocol. Clients are expected to use
|
5
|
+
# {https://www.npmjs.com/package/@rails/actioncable @rails/actioncable} to connect to the server.
|
6
|
+
#
|
7
|
+
# @see Rage::Cable::Protocols::Base
|
8
|
+
#
|
9
|
+
# @example Server side
|
10
|
+
# class TodoItemsChannel
|
11
|
+
# def subscribed
|
12
|
+
# stream_from "todo-items"
|
13
|
+
# end
|
9
14
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# * `on_message`
|
14
|
-
# * `serialize`
|
15
|
-
# * `subscribe`
|
16
|
-
# * `broadcast`
|
15
|
+
# def add_item(data)
|
16
|
+
# puts "Adding Todo item: #{data}"
|
17
|
+
# end
|
17
18
|
#
|
18
|
-
#
|
19
|
+
# def remove_item(data)
|
20
|
+
# puts "Removing Todo item: #{data}"
|
21
|
+
# end
|
22
|
+
# end
|
19
23
|
#
|
20
|
-
#
|
21
|
-
#
|
24
|
+
# @example Client side
|
25
|
+
# import { createConsumer } from '@rails/actioncable'
|
22
26
|
#
|
23
|
-
#
|
24
|
-
# should be extracted into another class. We'll refactor this once we start working on a new protocol.
|
27
|
+
# const cable = createConsumer('ws://localhost:3000/cable')
|
25
28
|
#
|
26
|
-
|
29
|
+
# const channel = cable.subscriptions.create('TodoItemsChannel', {
|
30
|
+
# connected: () => console.log('connected')
|
31
|
+
# })
|
32
|
+
#
|
33
|
+
# channel.perform('add_item', { item: 'New Item' })
|
34
|
+
# channel.perform('remove_item', { item_id: 123 })
|
35
|
+
#
|
36
|
+
class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
27
37
|
module TYPE
|
28
38
|
WELCOME = "welcome"
|
29
39
|
DISCONNECT = "disconnect"
|
@@ -59,7 +69,7 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
59
69
|
#
|
60
70
|
# @param router [Rage::Cable::Router]
|
61
71
|
def self.init(router)
|
62
|
-
|
72
|
+
super
|
63
73
|
|
64
74
|
Iodine.on_state(:on_start) do
|
65
75
|
ping_counter = Time.now.to_i
|
@@ -69,22 +79,6 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
69
79
|
Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json, Iodine::PubSub::PROCESS)
|
70
80
|
end
|
71
81
|
end
|
72
|
-
|
73
|
-
# Hash<String(stream name) => Set<Hash>(subscription params)>
|
74
|
-
@subscription_identifiers = Hash.new { |hash, key| hash[key] = Set.new }
|
75
|
-
|
76
|
-
# this is a fallback to synchronize subscription identifiers across different worker processes;
|
77
|
-
# we expect connections to be distributed among all workers, so this code will almost never be called;
|
78
|
-
# we also synchronize subscriptions with the master process so that the forks that are spun up instead
|
79
|
-
# of the crashed ones also had access to the identifiers;
|
80
|
-
Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
|
81
|
-
stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
|
82
|
-
@subscription_identifiers[stream_name] << params
|
83
|
-
end
|
84
|
-
|
85
|
-
Iodine.on_state(:on_finish) do
|
86
|
-
Iodine.unsubscribe("cable:synchronize")
|
87
|
-
end
|
88
82
|
end
|
89
83
|
|
90
84
|
# The method is called any time a new WebSocket connection is established.
|
@@ -146,7 +140,7 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
146
140
|
end
|
147
141
|
end
|
148
142
|
|
149
|
-
# The method should process client disconnections and call {Rage::Cable::Router#
|
143
|
+
# The method should process client disconnections and call {Rage::Cable::Router#process_disconnection}.
|
150
144
|
#
|
151
145
|
# @note This method is optional.
|
152
146
|
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
@@ -162,28 +156,4 @@ class Rage::Cable::Protocol::ActioncableV1Json
|
|
162
156
|
def self.serialize(params, data)
|
163
157
|
{ identifier: params.to_json, message: data }.to_json
|
164
158
|
end
|
165
|
-
|
166
|
-
# Subscribe to a stream.
|
167
|
-
#
|
168
|
-
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
169
|
-
# @param name [String] the stream name
|
170
|
-
# @param params [Hash] parameters associated with the client
|
171
|
-
def self.subscribe(connection, name, params)
|
172
|
-
connection.subscribe("cable:#{name}:#{Zlib.crc32(params.to_s)}")
|
173
|
-
|
174
|
-
unless @subscription_identifiers[name].include?(params)
|
175
|
-
@subscription_identifiers[name] << params
|
176
|
-
::Iodine.publish("cable:synchronize", [name, params].to_json)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# Broadcast data to all clients connected to a stream.
|
181
|
-
#
|
182
|
-
# @param name [String] the stream name
|
183
|
-
# @param data [Object] the data to send
|
184
|
-
def self.broadcast(name, data)
|
185
|
-
@subscription_identifiers[name].each do |params|
|
186
|
-
::Iodine.publish("cable:#{name}:#{Zlib.crc32(params.to_s)}", serialize(params, data))
|
187
|
-
end
|
188
|
-
end
|
189
159
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
##
|
6
|
+
# A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
|
7
|
+
# A protocol class should inherit from {Rage::Cable::Protocols::Base} and implement the following methods:
|
8
|
+
#
|
9
|
+
# * `on_open`
|
10
|
+
# * `on_message`
|
11
|
+
# * `serialize`
|
12
|
+
#
|
13
|
+
# The optional methods are:
|
14
|
+
#
|
15
|
+
# * `protocol_definition`
|
16
|
+
# * `on_shutdown`
|
17
|
+
# * `on_close`
|
18
|
+
#
|
19
|
+
class Rage::Cable::Protocols::Base
|
20
|
+
# @private
|
21
|
+
HANDSHAKE_HEADERS = {}
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# @param router [Rage::Cable::Router]
|
25
|
+
def init(router)
|
26
|
+
@router = router
|
27
|
+
|
28
|
+
# Hash<String(stream name) => Set<Hash>(subscription params)>
|
29
|
+
@subscription_identifiers = Hash.new { |hash, key| hash[key] = Set.new }
|
30
|
+
|
31
|
+
Iodine.on_state(:pre_start) do
|
32
|
+
# this is a fallback to synchronize subscription identifiers across different worker processes;
|
33
|
+
# we expect connections to be distributed among all workers, so this code will almost never be called;
|
34
|
+
# we also synchronize subscriptions with the master process so that the forks that are spun up instead
|
35
|
+
# of the crashed ones also had access to the identifiers;
|
36
|
+
Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
|
37
|
+
stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
|
38
|
+
@subscription_identifiers[stream_name] << params
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Iodine.on_state(:on_finish) do
|
43
|
+
Iodine.unsubscribe("cable:synchronize")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def protocol_definition
|
48
|
+
HANDSHAKE_HEADERS
|
49
|
+
end
|
50
|
+
|
51
|
+
# Subscribe to a stream.
|
52
|
+
#
|
53
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
54
|
+
# @param name [String] the stream name
|
55
|
+
# @param params [Hash] parameters associated with the client
|
56
|
+
def subscribe(connection, name, params)
|
57
|
+
connection.subscribe("cable:#{name}:#{stream_id(params)}")
|
58
|
+
|
59
|
+
unless @subscription_identifiers[name].include?(params)
|
60
|
+
@subscription_identifiers[name] << params
|
61
|
+
::Iodine.publish("cable:synchronize", [name, params].to_json)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Broadcast data to all clients connected to a stream.
|
66
|
+
#
|
67
|
+
# @param name [String] the stream name
|
68
|
+
# @param data [Object] the data to send
|
69
|
+
def broadcast(name, data)
|
70
|
+
@subscription_identifiers[name].each do |params|
|
71
|
+
::Iodine.publish("cable:#{name}:#{stream_id(params)}", serialize(params, data))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Whether the protocol allows remote procedure calls.
|
76
|
+
#
|
77
|
+
# @return [Boolean]
|
78
|
+
def supports_rpc?
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def stream_id(params)
|
85
|
+
Digest::MD5.hexdigest(params.to_s)
|
86
|
+
end
|
87
|
+
end # class << self
|
88
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# The `RawWebSocketJson` protocol allows a direct connection to a {Rage::Cable} application using the native
|
5
|
+
# `WebSocket` object. With this protocol, each WebSocket connection directly corresponds to a single
|
6
|
+
# channel subscription. As a result, clients are automatically subscribed to a channel as soon as
|
7
|
+
# they establish a connection.
|
8
|
+
#
|
9
|
+
# Heartbeats are also supported - the server will respond with `pong` to every `ping` message. Additionally,
|
10
|
+
# all ping messages are buffered and processed once a second, which means it can take up to a second for
|
11
|
+
# the server to respond to a `ping`.
|
12
|
+
#
|
13
|
+
# @see Rage::Cable::Protocols::Base
|
14
|
+
#
|
15
|
+
# @example Server side
|
16
|
+
# class TodoItemsChannel
|
17
|
+
# def subscribed
|
18
|
+
# stream_from "todo-items-#{params[:user_id]}"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def receive(data)
|
22
|
+
# puts "New Todo item: #{data}"
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @example Client side
|
27
|
+
# socket = new WebSocket("ws://localhost:3000/cable/todo_items?user_id=123")
|
28
|
+
# socket.send(JSON.stringify({ item: "New Item" }))
|
29
|
+
#
|
30
|
+
class Rage::Cable::Protocols::RawWebSocketJson < Rage::Cable::Protocols::Base
|
31
|
+
# identifiers are used to distinguish between different channels that share a single connection;
|
32
|
+
# since the raw protocol uses a single connection for each channel, identifiers are not necessary
|
33
|
+
IDENTIFIER = ""
|
34
|
+
private_constant :IDENTIFIER
|
35
|
+
|
36
|
+
module MESSAGES
|
37
|
+
UNAUTHORIZED = { err: "unauthorized" }.to_json
|
38
|
+
REJECTED = { err: "subscription rejected" }.to_json
|
39
|
+
INVALID = { err: "invalid channel name" }.to_json
|
40
|
+
UNKNOWN = { err: "unknown action" }.to_json
|
41
|
+
end
|
42
|
+
private_constant :MESSAGES
|
43
|
+
|
44
|
+
DEFAULT_PARAMS = {}.freeze
|
45
|
+
private_constant :DEFAULT_PARAMS
|
46
|
+
|
47
|
+
def self.init(router)
|
48
|
+
super
|
49
|
+
|
50
|
+
@ping_connections = Set.new
|
51
|
+
|
52
|
+
Iodine.on_state(:on_start) do
|
53
|
+
Iodine.run_every(1_000) do
|
54
|
+
@ping_connections.each_slice(500) do |slice|
|
55
|
+
Iodine.defer { slice.each { |connection| connection.write("pong") } }
|
56
|
+
end
|
57
|
+
|
58
|
+
@ping_connections.clear
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
64
|
+
def self.on_open(connection)
|
65
|
+
accepted = @router.process_connection(connection)
|
66
|
+
|
67
|
+
unless accepted
|
68
|
+
connection.write(MESSAGES::UNAUTHORIZED)
|
69
|
+
connection.close
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
channel_id = connection.env["PATH_INFO"].split("/")[-1]
|
74
|
+
|
75
|
+
channel_name = if channel_id.end_with?("Channel")
|
76
|
+
channel_id
|
77
|
+
else
|
78
|
+
if channel_id.include?("_")
|
79
|
+
tmp = ""
|
80
|
+
channel_id.split("_") { |segment| tmp += segment.capitalize! || segment }
|
81
|
+
channel_id = tmp
|
82
|
+
else
|
83
|
+
channel_id.capitalize!
|
84
|
+
end
|
85
|
+
|
86
|
+
"#{channel_id}Channel"
|
87
|
+
end
|
88
|
+
|
89
|
+
query_string = connection.env["QUERY_STRING"]
|
90
|
+
params = query_string == "" ? DEFAULT_PARAMS : Iodine::Rack::Utils.parse_nested_query(query_string)
|
91
|
+
|
92
|
+
status = @router.process_subscription(connection, IDENTIFIER, channel_name, params)
|
93
|
+
|
94
|
+
if status == :rejected
|
95
|
+
connection.write(MESSAGES::REJECTED)
|
96
|
+
connection.close
|
97
|
+
elsif status == :invalid
|
98
|
+
connection.write(MESSAGES::INVALID)
|
99
|
+
connection.close
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
104
|
+
# @param raw_data [String] the message body
|
105
|
+
def self.on_message(connection, raw_data)
|
106
|
+
if raw_data == "ping"
|
107
|
+
@ping_connections << connection
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
data = JSON.parse(raw_data)
|
112
|
+
|
113
|
+
message_status = @router.process_message(connection, IDENTIFIER, :receive, data)
|
114
|
+
unless message_status == :processed
|
115
|
+
connection.write(MESSAGES::UNKNOWN)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
120
|
+
def self.on_close(connection)
|
121
|
+
@router.process_disconnection(connection)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param data [Object] the object to serialize
|
125
|
+
def self.serialize(_, data)
|
126
|
+
data.to_json
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [Boolean]
|
130
|
+
def self.supports_rpc?
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
# The base implementation groups connection subscriptions by `params`;
|
136
|
+
# however, with `RawWebSocketJson`, params are not part of the payload (see {serialize})
|
137
|
+
# and we can disable grouping in exchange for better performance
|
138
|
+
#
|
139
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
140
|
+
# @param name [String] the stream name
|
141
|
+
def self.subscribe(connection, name, _)
|
142
|
+
super(connection, name, "")
|
143
|
+
end
|
144
|
+
end
|
data/lib/rage/configuration.rb
CHANGED
@@ -35,6 +35,15 @@ require "erb"
|
|
35
35
|
#
|
36
36
|
# > Defines one or several old secrets that need to be rotated. Can accept a single key or an array of keys. Rage will fall back to the `FALLBACK_SECRET_KEY_BASE` environment variable if this is not set.
|
37
37
|
#
|
38
|
+
# • _config.after_initialize_
|
39
|
+
#
|
40
|
+
# > Schedule a block of code to run after Rage has finished loading the application code. Use this to reference application-level constants during the initialization process.
|
41
|
+
# > ```
|
42
|
+
# Rage.config.after_initialize do
|
43
|
+
# SUPER_USER = User.find_by!(super: true)
|
44
|
+
# end
|
45
|
+
# > ```
|
46
|
+
#
|
38
47
|
# # Middleware Configuration
|
39
48
|
#
|
40
49
|
# • _config.middleware.use_
|
@@ -115,7 +124,7 @@ require "erb"
|
|
115
124
|
#
|
116
125
|
# • _config.cable.protocol_
|
117
126
|
#
|
118
|
-
# > Specifies the protocol the server will use.
|
127
|
+
# > Specifies the protocol the server will use. Supported values include {Rage::Cable::Protocols::ActioncableV1Json :actioncable_v1_json} and {Rage::Cable::Protocols::RawWebSocketJson :raw_websocket_json}. Defaults to {Rage::Cable::Protocols::ActioncableV1Json :actioncable_v1_json}.
|
119
128
|
#
|
120
129
|
# • _config.cable.allowed_request_origins_
|
121
130
|
#
|
@@ -153,6 +162,8 @@ require "erb"
|
|
153
162
|
# > Instructs Rage to not reuse Active Record connections between different fibers.
|
154
163
|
#
|
155
164
|
class Rage::Configuration
|
165
|
+
include Hooks
|
166
|
+
|
156
167
|
attr_accessor :logger
|
157
168
|
attr_reader :log_formatter, :log_level
|
158
169
|
attr_writer :secret_key_base, :fallback_secret_key_base
|
@@ -201,6 +212,14 @@ class Rage::Configuration
|
|
201
212
|
@internal ||= Internal.new
|
202
213
|
end
|
203
214
|
|
215
|
+
def after_initialize(&block)
|
216
|
+
push_hook(block, :after_initialize)
|
217
|
+
end
|
218
|
+
|
219
|
+
def run_after_initialize!
|
220
|
+
run_hooks_for!(:after_initialize, self)
|
221
|
+
end
|
222
|
+
|
204
223
|
class Server
|
205
224
|
attr_accessor :port, :workers_count, :timeout, :max_clients
|
206
225
|
attr_reader :threads_count
|
@@ -257,15 +276,29 @@ class Rage::Configuration
|
|
257
276
|
end
|
258
277
|
|
259
278
|
class Cable
|
260
|
-
attr_accessor :
|
279
|
+
attr_accessor :allowed_request_origins, :disable_request_forgery_protection
|
280
|
+
attr_reader :protocol
|
261
281
|
|
262
282
|
def initialize
|
263
|
-
@protocol = Rage::Cable::
|
283
|
+
@protocol = Rage::Cable::Protocols::ActioncableV1Json
|
264
284
|
@allowed_request_origins = if Rage.env.development? || Rage.env.test?
|
265
285
|
/localhost/
|
266
286
|
end
|
267
287
|
end
|
268
288
|
|
289
|
+
def protocol=(protocol)
|
290
|
+
@protocol = case protocol
|
291
|
+
when Class
|
292
|
+
protocol
|
293
|
+
when :actioncable_v1_json
|
294
|
+
Rage::Cable::Protocols::ActioncableV1Json
|
295
|
+
when :raw_websocket_json
|
296
|
+
Rage::Cable::Protocols::RawWebSocketJson
|
297
|
+
else
|
298
|
+
raise ArgumentError, "Unknown protocol. Supported values are `:actioncable_v1_json` and `:raw_websocket_json`."
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
269
302
|
# @private
|
270
303
|
def middlewares
|
271
304
|
@middlewares ||= begin
|
data/lib/rage/hooks.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
def hooks
|
5
|
+
@hooks ||= Hash.new { |h, k| h[k] = [] }
|
6
|
+
end
|
7
|
+
|
8
|
+
def push_hook(callback, hook_family)
|
9
|
+
hooks[hook_family] << callback if callback.is_a?(Proc)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_hooks_for!(hook_family, context = nil)
|
13
|
+
hooks[hook_family].each do |callback|
|
14
|
+
if context
|
15
|
+
context.instance_exec(&callback)
|
16
|
+
else
|
17
|
+
callback.call
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@hooks[hook_family] = []
|
22
|
+
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
@@ -20,7 +20,7 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
20
20
|
|
21
21
|
def __parse_nested(klass_str)
|
22
22
|
__parse(klass_str).tap { |visitor|
|
23
|
-
visitor.root_key = visitor.root_key_for_collection = visitor.key_transformer = nil
|
23
|
+
visitor.root_key = visitor.root_key_for_collection = visitor.root_key_proc = visitor.key_transformer = nil
|
24
24
|
}.build_schema
|
25
25
|
end
|
26
26
|
|
@@ -50,7 +50,7 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
50
50
|
end
|
51
51
|
|
52
52
|
class Visitor < Prism::Visitor
|
53
|
-
attr_accessor :schema, :root_key, :root_key_for_collection, :key_transformer, :collection_key, :meta
|
53
|
+
attr_accessor :schema, :root_key, :root_key_for_collection, :root_key_proc, :key_transformer, :collection_key, :meta
|
54
54
|
|
55
55
|
def initialize(parser, is_collection)
|
56
56
|
@parser = parser
|
@@ -64,6 +64,7 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
64
64
|
@self_name = nil
|
65
65
|
@root_key = nil
|
66
66
|
@root_key_for_collection = nil
|
67
|
+
@root_key_proc = nil
|
67
68
|
@key_transformer = nil
|
68
69
|
@collection_key = false
|
69
70
|
@meta = {}
|
@@ -74,7 +75,7 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
74
75
|
|
75
76
|
if node.name =~ /Resource$|Serializer$/ && node.superclass
|
76
77
|
visitor = @parser.__parse(node.superclass.name)
|
77
|
-
@root_key, @root_key_for_collection = visitor.root_key, visitor.root_key_for_collection
|
78
|
+
@root_key, @root_key_for_collection, @root_key_proc = visitor.root_key, visitor.root_key_for_collection, visitor.root_key_proc
|
78
79
|
@key_transformer, @collection_key, @meta = visitor.key_transformer, visitor.collection_key, visitor.meta
|
79
80
|
@schema.merge!(visitor.schema)
|
80
81
|
end
|
@@ -87,6 +88,13 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
87
88
|
|
88
89
|
result["properties"] = @schema if @schema.any?
|
89
90
|
|
91
|
+
if @root_key_proc
|
92
|
+
dynamic_root_key, dynamic_root_key_for_collection = @root_key_proc.call(@self_name)
|
93
|
+
|
94
|
+
@root_key = dynamic_root_key
|
95
|
+
@root_key_for_collection = dynamic_root_key_for_collection
|
96
|
+
end
|
97
|
+
|
90
98
|
if @is_collection
|
91
99
|
result = if @collection_key && @root_key_for_collection
|
92
100
|
{ "type" => "object", "properties" => { @root_key_for_collection => { "type" => "object", "additionalProperties" => result }, **@meta } }
|
@@ -109,6 +117,7 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
109
117
|
def visit_call_node(node)
|
110
118
|
case node.name
|
111
119
|
when :root_key
|
120
|
+
@root_key_proc = nil
|
112
121
|
context = with_context { visit(node.arguments) }
|
113
122
|
@root_key, @root_key_for_collection = context.symbols
|
114
123
|
|
@@ -135,12 +144,13 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
135
144
|
when :many, :has_many, :one, :has_one, :association
|
136
145
|
is_array = node.name == :many || node.name == :has_many
|
137
146
|
context = with_context { visit(node.arguments) }
|
138
|
-
|
147
|
+
association = context.symbols[0]
|
148
|
+
key = context.keywords["key"] || association
|
139
149
|
|
140
150
|
if node.block
|
141
151
|
with_inner_segment(key, is_array:) { visit(node.block) }
|
142
152
|
else
|
143
|
-
resource = context.keywords["resource"] || (::Alba.inflector && "#{::Alba.inflector.classify(
|
153
|
+
resource = context.keywords["resource"] || (::Alba.inflector && "#{::Alba.inflector.classify(association.to_s)}Resource")
|
144
154
|
is_valid_resource = @parser.namespace.const_get(resource) rescue false
|
145
155
|
|
146
156
|
@segment[key] = if is_array
|
@@ -159,10 +169,14 @@ class Rage::OpenAPI::Parsers::Ext::Alba
|
|
159
169
|
|
160
170
|
when :root_key!
|
161
171
|
if (inflector = ::Alba.inflector)
|
162
|
-
|
163
|
-
|
164
|
-
@
|
165
|
-
|
172
|
+
@root_key, @root_key_for_collection = nil
|
173
|
+
|
174
|
+
@root_key_proc = ->(resource_name) do
|
175
|
+
suffix = resource_name.end_with?("Resource") ? "Resource" : "Serializer"
|
176
|
+
name = inflector.demodulize(resource_name).delete_suffix(suffix)
|
177
|
+
|
178
|
+
inflector.underscore(name).yield_self { |key| [key, inflector.pluralize(key)] }
|
179
|
+
end
|
166
180
|
end
|
167
181
|
end
|
168
182
|
end
|
data/lib/rage/request.rb
CHANGED
data/lib/rage/router/node.rb
CHANGED
data/lib/rage/rspec.rb
CHANGED
data/lib/rage/setup.rb
CHANGED
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
data/rage.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.summary = "Fast web framework compatible with Rails."
|
12
12
|
spec.homepage = "https://github.com/rage-rb/rage"
|
13
13
|
spec.license = "MIT"
|
14
|
-
spec.required_ruby_version = ">= 3.
|
14
|
+
spec.required_ruby_version = ">= 3.2.0"
|
15
15
|
|
16
16
|
spec.metadata["homepage_uri"] = spec.homepage
|
17
17
|
spec.metadata["source_code_uri"] = "https://github.com/rage-rb/rage"
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rage-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Samoilov
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-05-20 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: thor
|
@@ -94,7 +93,6 @@ dependencies:
|
|
94
93
|
- - ">="
|
95
94
|
- !ruby/object:Gem::Version
|
96
95
|
version: '12.0'
|
97
|
-
description:
|
98
96
|
email:
|
99
97
|
- rsamoi@icloud.com
|
100
98
|
executables:
|
@@ -121,7 +119,9 @@ files:
|
|
121
119
|
- lib/rage/cable/cable.rb
|
122
120
|
- lib/rage/cable/channel.rb
|
123
121
|
- lib/rage/cable/connection.rb
|
124
|
-
- lib/rage/cable/
|
122
|
+
- lib/rage/cable/protocols/actioncable_v1_json.rb
|
123
|
+
- lib/rage/cable/protocols/base.rb
|
124
|
+
- lib/rage/cable/protocols/raw_web_socket_json.rb
|
125
125
|
- lib/rage/cable/router.rb
|
126
126
|
- lib/rage/cli.rb
|
127
127
|
- lib/rage/code_loader.rb
|
@@ -134,6 +134,7 @@ files:
|
|
134
134
|
- lib/rage/ext/setup.rb
|
135
135
|
- lib/rage/fiber.rb
|
136
136
|
- lib/rage/fiber_scheduler.rb
|
137
|
+
- lib/rage/hooks.rb
|
137
138
|
- lib/rage/logger/json_formatter.rb
|
138
139
|
- lib/rage/logger/logger.rb
|
139
140
|
- lib/rage/logger/text_formatter.rb
|
@@ -208,7 +209,6 @@ licenses:
|
|
208
209
|
metadata:
|
209
210
|
homepage_uri: https://github.com/rage-rb/rage
|
210
211
|
source_code_uri: https://github.com/rage-rb/rage
|
211
|
-
post_install_message:
|
212
212
|
rdoc_options: []
|
213
213
|
require_paths:
|
214
214
|
- lib
|
@@ -216,15 +216,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
216
216
|
requirements:
|
217
217
|
- - ">="
|
218
218
|
- !ruby/object:Gem::Version
|
219
|
-
version: 3.
|
219
|
+
version: 3.2.0
|
220
220
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
221
221
|
requirements:
|
222
222
|
- - ">="
|
223
223
|
- !ruby/object:Gem::Version
|
224
224
|
version: '0'
|
225
225
|
requirements: []
|
226
|
-
rubygems_version: 3.
|
227
|
-
signing_key:
|
226
|
+
rubygems_version: 3.6.2
|
228
227
|
specification_version: 4
|
229
228
|
summary: Fast web framework compatible with Rails.
|
230
229
|
test_files: []
|