rage-rb 1.15.1 → 1.17.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: 28c74a202068f676855b6837a45e5c32a614bad76c1af65178e3038f1f976439
4
- data.tar.gz: 53d023461db804902636c61711c49c25dde614d2b4d0ca112821ba61af9bdcf4
3
+ metadata.gz: 85fae5bbf87d602fb9cbde9e8356a6bfdb9d8e92ef02380f9124047a8d90d268
4
+ data.tar.gz: c6ff4ca55836d379609e07dfd9c2025de38aeccc9a29e14803551350f2348652
5
5
  SHA512:
6
- metadata.gz: eb4e1169d7a67bb0720f721279de1fd1729f5eb7f75c005b6ca4ecdb54a465bcb4ca97fa16acebc0ff256a1f4ebc7017a29cb00dfc254b6df3a3e8a1535e1e62
7
- data.tar.gz: 1133bae6fd831534848873917c61f5340bbe6ec65f446bcc07fa0b25c19c86b495afb03b029505ebe7d9569b50deb62dbca157d681f97fbeafab62a23b158d94
6
+ metadata.gz: 8d81759ffc54e6ede4b079dfed31dda024f274586451951aff72fa02e5cbf1014f31fc5760f07f7817dbf1db78262c88adeaff393020e54cecfd7126c169e0a2
7
+ data.tar.gz: fe7a3f9d392ed9343609b0c6c4c80624eb85c2df4be31690247355f11df185af2f9fe8ede04dc01583648f0e7cf75724742f2506a38c804618dbfd53c5fb1dde
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.17.0] - 2025-08-20
4
+
5
+ ### Added
6
+
7
+ - Add `Rage::Deferred` (#164).
8
+ - Add a controller generator by [@alex-rogachev](https://github.com/alex-rogachev) (#160).
9
+ - Update `stale?` to set cache headers by [@serhii-sadovskyi](https://github.com/serhii-sadovskyi) (#159).
10
+
11
+ ### Fixed
12
+
13
+ - Sub-millisecond sleep results in hang (#161).
14
+
15
+ ## [1.16.0] - 2025-05-20
16
+
17
+ ### Added
18
+
19
+ - [Cable] Add the `RawJSON` protocol (#150).
20
+ - Add the `after_initialize` hook by [@serhii-sadovskyi](https://github.com/serhii-sadovskyi) (#149).
21
+
22
+ ### Fixed
23
+
24
+ - Correctly parse plaintext responses in RSpec (#151).
25
+ - [OpenAPI] Correctly handle `root_key!` (#148).
26
+ - [OpenAPI] Correctly handle the `key` option in associations (#147).
27
+
3
28
  ## [1.15.1] - 2025-04-17
4
29
 
5
30
  ### Fixed
data/Gemfile CHANGED
@@ -12,6 +12,7 @@ gem "yard"
12
12
  gem "rubocop", "~> 1.65.0", require: false
13
13
 
14
14
  group :test do
15
+ gem "activesupport"
15
16
  gem "http"
16
17
  gem "pg"
17
18
  gem "mysql2"
data/OVERVIEW.md CHANGED
@@ -1,10 +1,18 @@
1
- ### Boot sequence and request lifecycle
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
  ![overview](https://github.com/rage-rb/rage/assets/2270393/0d45bbe3-622c-4b17-b8d8-552c567fecb3)
6
14
 
7
- ### Executing controller actions
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
+ ![cable](https://github.com/user-attachments/assets/86db2091-f93a-44f8-9512-c4701770d09e)
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
  [![Gem Version](https://badge.fury.io/rb/rage-rb.svg)](https://badge.fury.io/rb/rage-rb)
6
6
  ![Tests](https://github.com/rage-rb/rage/actions/workflows/main.yml/badge.svg)
7
- ![Ruby Requirement](https://img.shields.io/badge/Ruby-3.1%2B-%23f40000)
7
+ ![Ruby Requirement](https://img.shields.io/badge/Ruby-3.2%2B-%23f40000)
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
 
@@ -62,11 +62,12 @@ Built-in middleware:
62
62
  - [CORS](https://rage-rb.pages.dev/Rage/Cors)
63
63
  - [RequestId](https://rage-rb.pages.dev/Rage/RequestId)
64
64
 
65
- Also, see the following integration guides:
65
+ Also, see the following guides:
66
66
 
67
67
  - [Rails Integration](https://github.com/rage-rb/rage/wiki/Rails-integration)
68
68
  - [RSpec Integration](https://github.com/rage-rb/rage/wiki/RSpec-integration)
69
69
  - [WebSockets Guide](https://github.com/rage-rb/rage/wiki/WebSockets-guide)
70
+ - [Background Tasks Guide](https://github.com/rage-rb/rage/wiki/Background-Tasks-Guide)
70
71
 
71
72
  If you are a first-time contributor, make sure to check the [overview doc](https://github.com/rage-rb/rage/blob/master/OVERVIEW.md) that shows how Rage's core components interact with each other.
72
73
 
data/lib/rage/all.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative "../rage-rb"
2
2
 
3
3
  require_relative "version"
4
+ require_relative "hooks"
4
5
  require_relative "application"
5
6
  require_relative "fiber"
6
7
  require_relative "fiber_scheduler"
@@ -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.config.cable.protocol.broadcast(stream_name, JSON.parse(serialized_data))
110
+ Rage.cable.__protocol.broadcast(stream_name, JSON.parse(serialized_data))
111
111
  end
112
112
 
113
113
  last_id = id
@@ -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 Protocol
140
+ module Protocols
140
141
  end
142
+
143
+ Protocol = Protocols
141
144
  end
142
145
 
143
- require_relative "protocol/actioncable_v1_json"
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"
@@ -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.config.cable.protocol.subscribe(@__connection, stream, @__params)
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.config.cable.protocol.serialize(@__params, data)
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
- # A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
8
- # The class that defines a protocol should respond to the following methods:
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
- # * `protocol_definition`
11
- # * `init`
12
- # * `on_open`
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
- # The two optional methods are:
19
+ # def remove_item(data)
20
+ # puts "Removing Todo item: #{data}"
21
+ # end
22
+ # end
19
23
  #
20
- # * `on_shutdown`
21
- # * `on_close`
24
+ # @example Client side
25
+ # import { createConsumer } from '@rails/actioncable'
22
26
  #
23
- # It is likely that all logic around `@subscription_identifiers` has nothing to do with the protocol itself and
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
- class Rage::Cable::Protocol::ActioncableV1Json
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
- @router = router
72
+ super
63
73
 
64
74
  Iodine.on_state(:on_start) do
65
75
  ping_counter = Time.now.to_i
@@ -69,24 +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
- Iodine.on_state(:pre_start) do
77
- # this is a fallback to synchronize subscription identifiers across different worker processes;
78
- # we expect connections to be distributed among all workers, so this code will almost never be called;
79
- # we also synchronize subscriptions with the master process so that the forks that are spun up instead
80
- # of the crashed ones also had access to the identifiers;
81
- Iodine.subscribe("cable:synchronize") do |_, subscription_msg|
82
- stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
83
- @subscription_identifiers[stream_name] << params
84
- end
85
- end
86
-
87
- Iodine.on_state(:on_finish) do
88
- Iodine.unsubscribe("cable:synchronize")
89
- end
90
82
  end
91
83
 
92
84
  # The method is called any time a new WebSocket connection is established.
@@ -148,7 +140,7 @@ class Rage::Cable::Protocol::ActioncableV1Json
148
140
  end
149
141
  end
150
142
 
151
- # The method should process client disconnections and call {Rage::Cable::Router#process_message}.
143
+ # The method should process client disconnections and call {Rage::Cable::Router#process_disconnection}.
152
144
  #
153
145
  # @note This method is optional.
154
146
  # @param connection [Rage::Cable::WebSocketConnection] the connection object
@@ -164,28 +156,4 @@ class Rage::Cable::Protocol::ActioncableV1Json
164
156
  def self.serialize(params, data)
165
157
  { identifier: params.to_json, message: data }.to_json
166
158
  end
167
-
168
- # Subscribe to a stream.
169
- #
170
- # @param connection [Rage::Cable::WebSocketConnection] the connection object
171
- # @param name [String] the stream name
172
- # @param params [Hash] parameters associated with the client
173
- def self.subscribe(connection, name, params)
174
- connection.subscribe("cable:#{name}:#{Zlib.crc32(params.to_s)}")
175
-
176
- unless @subscription_identifiers[name].include?(params)
177
- @subscription_identifiers[name] << params
178
- ::Iodine.publish("cable:synchronize", [name, params].to_json)
179
- end
180
- end
181
-
182
- # Broadcast data to all clients connected to a stream.
183
- #
184
- # @param name [String] the stream name
185
- # @param data [Object] the data to send
186
- def self.broadcast(name, data)
187
- @subscription_identifiers[name].each do |params|
188
- ::Iodine.publish("cable:#{name}:#{Zlib.crc32(params.to_s)}", serialize(params, data))
189
- end
190
- end
191
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/cli.rb CHANGED
@@ -30,6 +30,26 @@ module Rage
30
30
  template("model-template/model.rb", "app/models/#{name.singularize.underscore}.rb")
31
31
  end
32
32
 
33
+ desc "controller NAME", "Generate a new controller."
34
+ def controller(name = nil)
35
+ return help("controller") if name.nil?
36
+
37
+ setup
38
+ unless defined?(ActiveSupport::Inflector)
39
+ raise LoadError, <<~ERR
40
+ ActiveSupport::Inflector is required to run this command. Add the following line to your Gemfile:
41
+ gem "activesupport", require: "active_support/inflector"
42
+ ERR
43
+ end
44
+
45
+ # remove trailing Controller if already present
46
+ normalized_name = name.sub(/_?controller$/i, "")
47
+ @controller_name = "#{normalized_name.camelize}Controller"
48
+ file_name = "#{normalized_name.underscore}_controller.rb"
49
+
50
+ template("controller-template/controller.rb", "app/controllers/#{file_name}")
51
+ end
52
+
33
53
  private
34
54
 
35
55
  def setup
@@ -22,6 +22,8 @@ class Rage::CodeLoader
22
22
  @loader.enable_reloading if enable_reloading
23
23
  @loader.setup
24
24
  @loader.eager_load if enable_eager_loading
25
+
26
+ configure_components
25
27
  end
26
28
 
27
29
  # in standalone mode - reload the code and the routes
@@ -34,13 +36,7 @@ class Rage::CodeLoader
34
36
  Rage.__router.reset_routes
35
37
  load("#{Rage.root}/config/routes.rb")
36
38
 
37
- unless Rage.autoload?(:Cable) # the `Cable` component is loaded
38
- Rage::Cable.__router.reset
39
- end
40
-
41
- unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
42
- Rage::OpenAPI.__reset_data_cache
43
- end
39
+ reload_components
44
40
  end
45
41
 
46
42
  # in Rails mode - reset the routes; everything else will be done by Rails
@@ -50,13 +46,7 @@ class Rage::CodeLoader
50
46
  @reloading = true
51
47
  Rage.__router.reset_routes
52
48
 
53
- unless Rage.autoload?(:Cable) # the `Cable` component is loaded
54
- Rage::Cable.__router.reset
55
- end
56
-
57
- unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
58
- Rage::OpenAPI.__reset_data_cache
59
- end
49
+ reload_components
60
50
  end
61
51
 
62
52
  def reloading?
@@ -73,4 +63,25 @@ class Rage::CodeLoader
73
63
  ensure
74
64
  @last_watched, @last_update_at = current_watched, current_update_at
75
65
  end
66
+
67
+ private
68
+
69
+ def configure_components
70
+ if Rage.env.development? && (Rage.config.deferred.configured? || Rage.config.deferred.has_default_disk_storage?)
71
+ # if there's at least one task, `Rage::Deferred` will be automatically loaded in production;
72
+ # in development, however, eager loading is disabled, and we want to automatically load
73
+ # the module in case it was explicitly configured or if a disk storage exists
74
+ Rage::Deferred
75
+ end
76
+ end
77
+
78
+ def reload_components
79
+ unless Rage.autoload?(:Cable) # the `Cable` component is loaded
80
+ Rage::Cable.__router.reset
81
+ end
82
+
83
+ unless Rage.autoload?(:OpenAPI) # the `OpenAPI` component is loaded
84
+ Rage::OpenAPI.__reset_data_cache
85
+ end
86
+ end
76
87
  end