rage-rb 1.28.2 → 1.29.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 +15 -0
- data/Gemfile +1 -1
- data/lib/rage/cable/cable.rb +84 -24
- data/lib/rage/cable/channel.rb +15 -9
- data/lib/rage/cable/connection.rb +62 -1
- data/lib/rage/cable/protocols/actioncable_v1_json.rb +36 -15
- data/lib/rage/cable/protocols/base.rb +18 -10
- data/lib/rage/cable/protocols/raw_web_socket_json.rb +22 -10
- data/lib/rage/cable/remote_disconnect.rb +69 -0
- data/lib/rage/cable/router.rb +42 -29
- data/lib/rage/code_loader.rb +1 -1
- data/lib/rage/configuration.rb +20 -19
- data/lib/rage/cookies.rb +11 -1
- data/lib/rage/daemon.rb +6 -5
- data/lib/rage/deferred/backends/disk.rb +215 -91
- data/lib/rage/deferred/backends/nil.rb +2 -2
- data/lib/rage/deferred/dead_task.rb +96 -0
- data/lib/rage/deferred/dead_tasks.rb +44 -0
- data/lib/rage/deferred/deferred.rb +28 -1
- data/lib/rage/extension.rb +8 -0
- data/lib/rage/fiber.rb +1 -1
- data/lib/rage/fiber_scheduler.rb +53 -39
- data/lib/rage/internal.rb +4 -0
- data/lib/rage/middleware/origin_validator.rb +35 -12
- data/lib/rage/pubsub/pubsub.rb +146 -1
- data/lib/rage/router/dsl.rb +4 -1
- data/lib/rage/rspec.rb +1 -6
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +6 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3042d8b6494404e49b19c70a5ee464e0b8f35a932bd1d5eff76a8fae8bc0713f
|
|
4
|
+
data.tar.gz: 4d69ac24a0dbd116c3cea590ecaad949fed45abe379660ab820ed8b5cbd05eec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf528a5e500571e0ff1f8662b15edcce821561810efa0d6ff00c74b05909e5fceee7dad8d2c4e6ddf4d57d46ba7f9548ac5a26159b1c531344f2b9701baad6e9
|
|
7
|
+
data.tar.gz: 8b6675e67d81d59f2444d393659e35569f2ab73a24e399c8ad268c97477c4ee315cc7a6fd86185cb94fa88f74bc2906366fe9c69e10a742abb75c074a50cac8e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.29.0] - 2026-10-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- [Deferred] Add `Rage::Deferred.dead_tasks` for listing and inspecting tasks that exhausted their retries.
|
|
8
|
+
- Add low-level `PubSub` API (#399).
|
|
9
|
+
- [Cable] Serialize WS messages per connection (#400).
|
|
10
|
+
- Update fiber scheduler to support Ruby 4.1 (#397).
|
|
11
|
+
- Improve blocking mechanism for fibers (#401).
|
|
12
|
+
- Support multiple isolated cable apps (#406).
|
|
13
|
+
- [Daemon] Improve cleanup mechanism (#410).
|
|
14
|
+
- [Cable] Support remote disconnect for `Rage::Cable` (#409).
|
|
15
|
+
- [Cable] Support `allow_same_origin_as_host` (#412).
|
|
16
|
+
- Skip FiberWrapper when building application from non-root fiber (#413).
|
|
17
|
+
|
|
3
18
|
## [1.28.2] - 2026-10-09
|
|
4
19
|
|
|
5
20
|
- Support `unsubscribe` commands in Action Cable.
|
data/Gemfile
CHANGED
data/lib/rage/cable/cable.rb
CHANGED
|
@@ -38,9 +38,21 @@ module Rage::Cable
|
|
|
38
38
|
# map "/cable" do
|
|
39
39
|
# run Rage.cable.application
|
|
40
40
|
# end
|
|
41
|
-
def self.application
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
def self.application(namespace: Object, protocol: nil)
|
|
42
|
+
unless namespace.is_a?(Module)
|
|
43
|
+
raise ArgumentError, "The `namespace` option must be a Module, got #{namespace.class}."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protocol = __protocol_for(protocol || Rage.config.cable.protocol)
|
|
47
|
+
|
|
48
|
+
unless __protocols.include?(protocol)
|
|
49
|
+
__protocols << protocol.tap(&:init)
|
|
50
|
+
@__adapter&.add_broadcaster(PUBSUB_BROADCASTER_ID, Relay)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
router = Router.new(protocol, namespace)
|
|
54
|
+
handler = __build_handler(protocol, router)
|
|
55
|
+
accept_response = [101, protocol.protocol_definition, []]
|
|
44
56
|
|
|
45
57
|
application = ->(env) do
|
|
46
58
|
Rage::Telemetry.tracer.span_cable_websocket_handshake(env:) do
|
|
@@ -55,8 +67,15 @@ module Rage::Cable
|
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
chain = Rage.with_middlewares(application, Rage.config.cable.middlewares)
|
|
58
|
-
|
|
59
|
-
chain.define_singleton_method(:__rage_root_app)
|
|
70
|
+
|
|
71
|
+
chain.define_singleton_method(:__rage_root_app) do
|
|
72
|
+
root_app = Rage.with_middlewares(application, Rage.config.cable.middlewares(include_http_middleware: false))
|
|
73
|
+
root_app.define_singleton_method(:__rage_app_name) do
|
|
74
|
+
namespace == Object ? "Rage::Cable" : "Rage::Cable(namespace: #{namespace})"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
root_app
|
|
78
|
+
end
|
|
60
79
|
|
|
61
80
|
chain
|
|
62
81
|
end
|
|
@@ -64,25 +83,35 @@ module Rage::Cable
|
|
|
64
83
|
# @private
|
|
65
84
|
def self.__initialize
|
|
66
85
|
if (adapter = Rage.config.pubsub.adapter)
|
|
67
|
-
adapter.add_broadcaster(PUBSUB_BROADCASTER_ID,
|
|
86
|
+
adapter.add_broadcaster(PUBSUB_BROADCASTER_ID, Relay)
|
|
68
87
|
@__adapter = adapter
|
|
69
88
|
end
|
|
70
89
|
end
|
|
71
90
|
|
|
72
91
|
# @private
|
|
73
|
-
def self.
|
|
74
|
-
@
|
|
92
|
+
def self.__protocols
|
|
93
|
+
@__protocols ||= []
|
|
75
94
|
end
|
|
76
95
|
|
|
77
96
|
# @private
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
# @param protocol [:actioncable_v1_json, :raw_websocket_json, Class]
|
|
98
|
+
def self.__protocol_for(protocol)
|
|
99
|
+
case protocol
|
|
100
|
+
when Class
|
|
101
|
+
protocol
|
|
102
|
+
when :actioncable_v1_json
|
|
103
|
+
Protocols::ActioncableV1Json
|
|
104
|
+
when :raw_websocket_json
|
|
105
|
+
Protocols::RawWebSocketJson
|
|
106
|
+
else
|
|
107
|
+
raise ArgumentError, "Unknown protocol. Supported values are `:actioncable_v1_json` and `:raw_websocket_json`."
|
|
108
|
+
end
|
|
80
109
|
end
|
|
81
110
|
|
|
82
111
|
# @private
|
|
83
|
-
def self.__build_handler(protocol)
|
|
112
|
+
def self.__build_handler(protocol, router)
|
|
84
113
|
klass = Class.new do
|
|
85
|
-
def initialize(protocol)
|
|
114
|
+
def initialize(protocol, router)
|
|
86
115
|
Iodine.on_state(:on_start) do
|
|
87
116
|
unless Fiber.scheduler
|
|
88
117
|
Fiber.set_scheduler(Rage::FiberScheduler.new)
|
|
@@ -90,27 +119,30 @@ module Rage::Cable
|
|
|
90
119
|
end
|
|
91
120
|
|
|
92
121
|
@protocol = protocol
|
|
122
|
+
@router = router
|
|
93
123
|
@log_processor = Rage.__log_processor
|
|
94
124
|
end
|
|
95
125
|
|
|
96
126
|
def on_open(connection)
|
|
97
|
-
|
|
127
|
+
init_connection(connection)
|
|
128
|
+
schedule_processing(connection) { |env| @protocol.on_open(connection, env, @router) }
|
|
98
129
|
end
|
|
99
130
|
|
|
100
131
|
def on_message(connection, data)
|
|
101
|
-
|
|
132
|
+
schedule_processing(connection) { |env| @protocol.on_message(connection, env, data, @router) }
|
|
102
133
|
end
|
|
103
134
|
|
|
104
135
|
if protocol.respond_to?(:on_close)
|
|
105
136
|
def on_close(connection)
|
|
106
137
|
return unless ::Iodine.running?
|
|
107
|
-
|
|
138
|
+
schedule_processing(connection) { |env| @protocol.on_close(connection, env, @router) }
|
|
139
|
+
connection.env["rage.cable.queue"] << nil
|
|
108
140
|
end
|
|
109
141
|
end
|
|
110
142
|
|
|
111
143
|
if protocol.respond_to?(:on_shutdown)
|
|
112
144
|
def on_shutdown(connection)
|
|
113
|
-
@protocol.on_shutdown(connection)
|
|
145
|
+
@protocol.on_shutdown(connection, @router)
|
|
114
146
|
rescue => e
|
|
115
147
|
log_error(e)
|
|
116
148
|
end
|
|
@@ -118,13 +150,31 @@ module Rage::Cable
|
|
|
118
150
|
|
|
119
151
|
private
|
|
120
152
|
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
def init_connection(connection)
|
|
154
|
+
queue = Queue.new
|
|
155
|
+
fiber = Fiber.schedule do
|
|
156
|
+
# `connection.env` is reset once the connection is closed; since messages are processed asynchronously,
|
|
157
|
+
# a connection can already be closed by the time the message is processed, so we store `env` separately
|
|
158
|
+
env = connection.env
|
|
159
|
+
|
|
160
|
+
Rage.__log_processor.init_request_logger(env)
|
|
161
|
+
|
|
162
|
+
loop do
|
|
163
|
+
work = queue.shift
|
|
164
|
+
break unless work
|
|
165
|
+
|
|
166
|
+
work.call(env)
|
|
167
|
+
rescue => e
|
|
168
|
+
log_error(e)
|
|
169
|
+
end
|
|
127
170
|
end
|
|
171
|
+
|
|
172
|
+
connection.env["rage.cable.fiber"] = fiber
|
|
173
|
+
connection.env["rage.cable.queue"] = queue
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def schedule_processing(connection, &block)
|
|
177
|
+
connection.env["rage.cable.queue"] << block
|
|
128
178
|
end
|
|
129
179
|
|
|
130
180
|
def log_error(e)
|
|
@@ -133,7 +183,7 @@ module Rage::Cable
|
|
|
133
183
|
end
|
|
134
184
|
end
|
|
135
185
|
|
|
136
|
-
klass.new(protocol)
|
|
186
|
+
klass.new(protocol, router)
|
|
137
187
|
end
|
|
138
188
|
|
|
139
189
|
# Broadcast data directly to a named stream.
|
|
@@ -145,13 +195,22 @@ module Rage::Cable
|
|
|
145
195
|
# Rage.cable.broadcast("chat", { message: "A new member has joined!" })
|
|
146
196
|
def self.broadcast(stream, data)
|
|
147
197
|
Rage::Telemetry.tracer.span_cable_stream_broadcast(stream:) do
|
|
148
|
-
|
|
198
|
+
Relay.broadcast(stream, data)
|
|
149
199
|
@__adapter&.publish(PUBSUB_BROADCASTER_ID, stream, data)
|
|
150
200
|
end
|
|
151
201
|
|
|
152
202
|
true
|
|
153
203
|
end
|
|
154
204
|
|
|
205
|
+
# @private
|
|
206
|
+
module Relay
|
|
207
|
+
def self.broadcast(stream, data)
|
|
208
|
+
Rage::Cable.__protocols.each do |protocol|
|
|
209
|
+
protocol.broadcast(stream, data)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
155
214
|
# @!parse [ruby]
|
|
156
215
|
# # @abstract
|
|
157
216
|
# class WebSocketConnection
|
|
@@ -190,6 +249,7 @@ require_relative "protocols/raw_web_socket_json"
|
|
|
190
249
|
require_relative "channel"
|
|
191
250
|
require_relative "connection"
|
|
192
251
|
require_relative "router"
|
|
252
|
+
require_relative "remote_disconnect"
|
|
193
253
|
|
|
194
254
|
if Rage.config.internal.initialized?
|
|
195
255
|
Rage::Cable.__initialize
|
data/lib/rage/cable/channel.rb
CHANGED
|
@@ -4,19 +4,18 @@ class Rage::Cable::Channel
|
|
|
4
4
|
|
|
5
5
|
class << self
|
|
6
6
|
# @private
|
|
7
|
-
attr_reader :__prepared_actions
|
|
8
|
-
|
|
9
|
-
# @private
|
|
10
|
-
attr_reader :__channels
|
|
7
|
+
attr_reader :__prepared_actions, :__channels, :__protocol
|
|
11
8
|
|
|
12
9
|
# @private
|
|
13
10
|
# returns a list of actions that can be called remotely
|
|
14
|
-
def __register_actions
|
|
11
|
+
def __register_actions(protocol)
|
|
12
|
+
@__protocol = protocol
|
|
13
|
+
|
|
15
14
|
actions = (
|
|
16
15
|
public_instance_methods(true) - Rage::Cable::Channel.public_instance_methods(true)
|
|
17
16
|
).reject { |m| m.start_with?("__rage_tmp") || m.start_with?("__run") }
|
|
18
17
|
|
|
19
|
-
actions.reject! { |m| m != :receive } unless
|
|
18
|
+
actions.reject! { |m| m != :receive } unless @__protocol.supports_rpc?
|
|
20
19
|
|
|
21
20
|
@__prepared_actions = (INTERNAL_ACTIONS + actions).each_with_object({}) do |action_name, memo|
|
|
22
21
|
memo[action_name] = __register_action_proc(action_name)
|
|
@@ -417,6 +416,13 @@ class Rage::Cable::Channel
|
|
|
417
416
|
@__params
|
|
418
417
|
end
|
|
419
418
|
|
|
419
|
+
# Get the Rack env hash that is attached to the underlying WebSocket connection.
|
|
420
|
+
#
|
|
421
|
+
# @return [Hash]
|
|
422
|
+
def env
|
|
423
|
+
@__connection.env
|
|
424
|
+
end
|
|
425
|
+
|
|
420
426
|
# Reject the subscription request. The method should only be called during the subscription
|
|
421
427
|
# process (i.e. inside the {subscribed} method or {before_subscribe}/{after_subscribe} hooks).
|
|
422
428
|
def reject
|
|
@@ -449,7 +455,7 @@ class Rage::Cable::Channel
|
|
|
449
455
|
# Rage::Cable.broadcast("notifications", { message: "A new member has joined!" })
|
|
450
456
|
def stream_from(stream)
|
|
451
457
|
raise ArgumentError, "Stream name must be a String" unless stream.is_a?(String)
|
|
452
|
-
|
|
458
|
+
self.class.__protocol.subscribe(@__connection, stream, @__params)
|
|
453
459
|
end
|
|
454
460
|
|
|
455
461
|
# 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.
|
|
@@ -490,7 +496,7 @@ class Rage::Cable::Channel
|
|
|
490
496
|
# end
|
|
491
497
|
def stop_stream_from(stream)
|
|
492
498
|
raise ArgumentError, "Stream name must be a String" unless stream.is_a?(String)
|
|
493
|
-
|
|
499
|
+
self.class.__protocol.unsubscribe(@__connection, stream, @__params)
|
|
494
500
|
end
|
|
495
501
|
|
|
496
502
|
# Unsubscribe from a local stream. The counterpart to {stream_for}.
|
|
@@ -527,7 +533,7 @@ class Rage::Cable::Channel
|
|
|
527
533
|
# transmit({ message: "Hello!" })
|
|
528
534
|
# end
|
|
529
535
|
def transmit(data)
|
|
530
|
-
message =
|
|
536
|
+
message = self.class.__protocol.serialize(@__params, data)
|
|
531
537
|
|
|
532
538
|
if @__is_subscribing
|
|
533
539
|
# we expect a confirmation message to be sent as a result of a successful subscribe call;
|
|
@@ -4,18 +4,27 @@ class Rage::Cable::Connection
|
|
|
4
4
|
# @private
|
|
5
5
|
attr_reader :__identified_by_map
|
|
6
6
|
|
|
7
|
+
class << self
|
|
8
|
+
# @private
|
|
9
|
+
attr_reader :__identifiers
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
|
|
8
13
|
# Common identifiers are `current_user` and `current_account`, but could be anything.
|
|
9
14
|
#
|
|
10
15
|
# @param identifiers [Symbol,Array<Symbol>]
|
|
11
16
|
def self.identified_by(*identifiers)
|
|
12
17
|
identifiers.each do |method_name|
|
|
18
|
+
raise ArgumentError, "Identifier must be a Symbol" unless method_name.is_a?(Symbol)
|
|
19
|
+
|
|
20
|
+
(@__identifiers ||= []) << method_name
|
|
21
|
+
|
|
13
22
|
define_method(method_name) do
|
|
14
23
|
@__identified_by_map[method_name]
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
define_method("#{method_name}=") do |data|
|
|
18
|
-
@__identified_by_map[method_name] = data
|
|
27
|
+
@__identified_by_map[method_name] = data if data
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
Rage::Cable::Channel.__prepare_id_method(method_name)
|
|
@@ -79,4 +88,56 @@ class Rage::Cable::Connection
|
|
|
79
88
|
raise "Cookies cannot be set for WebSocket clients"
|
|
80
89
|
end
|
|
81
90
|
end
|
|
91
|
+
|
|
92
|
+
# @private
|
|
93
|
+
def __connect(connection, protocol)
|
|
94
|
+
Rage::Telemetry.tracer.span_cable_connection_process(connection: self, action: :connect, env: @__env) do
|
|
95
|
+
connect
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
__remote_disconnect_setup(connection, protocol) if !rejected? && is_a?(Rage::Cable::RemoteDisconnect)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @private
|
|
102
|
+
def __disconnect(connection)
|
|
103
|
+
Rage::Telemetry.tracer.span_cable_connection_process(connection: self, action: :disconnect, env: @__env) do
|
|
104
|
+
disconnect
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
__remote_disconnect_cleanup(connection) if is_a?(Rage::Cable::RemoteDisconnect)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Disconnect all connections matching the specified identifier.
|
|
111
|
+
#
|
|
112
|
+
# Only one identifier can be provided per call. To disconnect connections matching multiple
|
|
113
|
+
# criteria, define a composite identifier.
|
|
114
|
+
#
|
|
115
|
+
# @note This method is only available when {Rage::Cable::RemoteDisconnect Rage::Cable::RemoteDisconnect} is
|
|
116
|
+
# included in your connection class.
|
|
117
|
+
#
|
|
118
|
+
# @example Disconnect all connections for a user
|
|
119
|
+
# RageCable::Connection.disconnect(current_user: User.find(1))
|
|
120
|
+
#
|
|
121
|
+
# @example Using a composite identifier
|
|
122
|
+
# class RageCable::Connection < Rage::Cable::Connection
|
|
123
|
+
# include Rage::Cable::RemoteDisconnect
|
|
124
|
+
#
|
|
125
|
+
# identified_by :current_user, :current_user_device
|
|
126
|
+
#
|
|
127
|
+
# def connect
|
|
128
|
+
# self.current_user = find_user
|
|
129
|
+
# self.current_user_device = [current_user, request.headers["Device-Id"]]
|
|
130
|
+
# end
|
|
131
|
+
# end
|
|
132
|
+
#
|
|
133
|
+
# # Disconnect only the user's specific device
|
|
134
|
+
# RageCable::Connection.disconnect(current_user_device: [user, "device-123"])
|
|
135
|
+
#
|
|
136
|
+
# @example Using protocol options
|
|
137
|
+
# RageCable::Connection.disconnect(current_user: User.find(1), protocol_options: { reconnect: false })
|
|
138
|
+
#
|
|
139
|
+
# @param identified_by [Hash] a single key-value pair where the key is a declared identifier
|
|
140
|
+
# @param protocol_options [Hash] options passed to the connection's protocol when disconnecting
|
|
141
|
+
# @raise [ArgumentError] if no identifier, multiple identifiers, or an unknown identifier is provided
|
|
142
|
+
# @!method self.disconnect(protocol_options: nil, **identified_by)
|
|
82
143
|
end
|
|
@@ -45,6 +45,7 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
45
45
|
module REASON
|
|
46
46
|
UNAUTHORIZED = "unauthorized"
|
|
47
47
|
INVALID = "invalid_request"
|
|
48
|
+
REMOTE = "remote"
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
module COMMAND
|
|
@@ -57,6 +58,8 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
57
58
|
WELCOME = { type: TYPE::WELCOME }.to_json
|
|
58
59
|
UNAUTHORIZED = { type: TYPE::DISCONNECT, reason: REASON::UNAUTHORIZED, reconnect: false }.to_json
|
|
59
60
|
INVALID = { type: TYPE::DISCONNECT, reason: REASON::INVALID, reconnect: true }.to_json
|
|
61
|
+
DISCONNECT = { type: TYPE::DISCONNECT, reason: REASON::REMOTE, reconnect: true }.to_json
|
|
62
|
+
DISCONNECT_NO_RECONNECT = { type: TYPE::DISCONNECT, reason: REASON::REMOTE, reconnect: false }.to_json
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
HANDSHAKE_HEADERS = { "Sec-WebSocket-Protocol" => "actioncable-v1-json" }
|
|
@@ -67,9 +70,7 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
# This method serves as a constructor to prepare the object or set up recurring tasks (e.g. heartbeats).
|
|
70
|
-
|
|
71
|
-
# @param router [Rage::Cable::Router]
|
|
72
|
-
def self.init(router)
|
|
73
|
+
def self.init
|
|
73
74
|
super
|
|
74
75
|
|
|
75
76
|
Iodine.on_state(:on_start) do
|
|
@@ -86,9 +87,10 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
86
87
|
# It is expected to call {Rage::Cable::Router#process_connection} and handle its return value.
|
|
87
88
|
#
|
|
88
89
|
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
|
89
|
-
# @
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
# @param env [Hash] the Rack environment
|
|
91
|
+
# @param router [Rage::Cable::Router] the Cable router
|
|
92
|
+
def self.on_open(connection, env, router)
|
|
93
|
+
accepted = router.process_connection(connection, env)
|
|
92
94
|
|
|
93
95
|
if accepted
|
|
94
96
|
connection.subscribe("cable:ping")
|
|
@@ -103,9 +105,10 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
103
105
|
# {Rage::Cable::Router#process_subscription} or {Rage::Cable::Router#process_message}, and handle its return value.
|
|
104
106
|
#
|
|
105
107
|
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
|
108
|
+
# @param env [Hash] the Rack environment
|
|
106
109
|
# @param raw_data [String] the message body
|
|
107
|
-
# @
|
|
108
|
-
def self.on_message(connection, raw_data)
|
|
110
|
+
# @param router [Rage::Cable::Router] the Cable router
|
|
111
|
+
def self.on_message(connection, env, raw_data, router)
|
|
109
112
|
parsed_data = Rage::ParamsParser.json_parse(raw_data)
|
|
110
113
|
|
|
111
114
|
command, identifier = parsed_data[:command], parsed_data[:identifier]
|
|
@@ -113,7 +116,7 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
113
116
|
|
|
114
117
|
# process subscription messages
|
|
115
118
|
if command == COMMAND::SUBSCRIBE
|
|
116
|
-
status =
|
|
119
|
+
status = router.process_subscription(connection, env, identifier, params[:channel], params)
|
|
117
120
|
if status == :subscribed
|
|
118
121
|
connection.write({ identifier: identifier, type: TYPE::CONFIRM }.to_json)
|
|
119
122
|
elsif status == :rejected
|
|
@@ -124,7 +127,7 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
124
127
|
|
|
125
128
|
return
|
|
126
129
|
elsif command == COMMAND::UNSUBSCRIBE
|
|
127
|
-
|
|
130
|
+
router.process_unsubscription(connection, env, identifier)
|
|
128
131
|
return
|
|
129
132
|
end
|
|
130
133
|
|
|
@@ -133,10 +136,10 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
133
136
|
data = JSON.parse(parsed_data[:data])
|
|
134
137
|
|
|
135
138
|
message_status = if command == COMMAND::MESSAGE && data.has_key?("action")
|
|
136
|
-
|
|
139
|
+
router.process_message(connection, env, identifier, data["action"].to_sym, data)
|
|
137
140
|
|
|
138
141
|
elsif command == COMMAND::MESSAGE
|
|
139
|
-
|
|
142
|
+
router.process_message(connection, env, identifier, :receive, data)
|
|
140
143
|
end
|
|
141
144
|
|
|
142
145
|
unless message_status == :processed
|
|
@@ -148,9 +151,10 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
148
151
|
#
|
|
149
152
|
# @note This method is optional.
|
|
150
153
|
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
|
151
|
-
# @
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
# @param env [Hash] the Rack environment
|
|
155
|
+
# @param router [Rage::Cable::Router] the Cable router
|
|
156
|
+
def self.on_close(connection, env, router)
|
|
157
|
+
router.process_disconnection(connection, env)
|
|
154
158
|
end
|
|
155
159
|
|
|
156
160
|
# Serialize a Ruby object into the format the client would understand.
|
|
@@ -160,4 +164,21 @@ class Rage::Cable::Protocols::ActioncableV1Json < Rage::Cable::Protocols::Base
|
|
|
160
164
|
def self.serialize(params, data)
|
|
161
165
|
{ identifier: params.to_json, message: data }.to_json
|
|
162
166
|
end
|
|
167
|
+
|
|
168
|
+
# A unique identifier of the protocol used in multi-server setups by {Rage::PubSub}.
|
|
169
|
+
# @return [String]
|
|
170
|
+
def self.protocol_id
|
|
171
|
+
"acv1"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Process the disconnect initiated by the server.
|
|
175
|
+
#
|
|
176
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
|
177
|
+
# @param options [Hash, nil] options passed to the protocol when disconnecting
|
|
178
|
+
def self.on_remote_disconnect(connection, options)
|
|
179
|
+
reconnect = options.nil? || options.fetch(:reconnect, true)
|
|
180
|
+
message = reconnect ? MESSAGES::DISCONNECT : MESSAGES::DISCONNECT_NO_RECONNECT
|
|
181
|
+
|
|
182
|
+
connection.write(message)
|
|
183
|
+
end
|
|
163
184
|
end
|
|
@@ -9,38 +9,39 @@ require "digest"
|
|
|
9
9
|
# * `on_open`
|
|
10
10
|
# * `on_message`
|
|
11
11
|
# * `serialize`
|
|
12
|
+
# * `protocol_id`
|
|
12
13
|
#
|
|
13
14
|
# The optional methods are:
|
|
14
15
|
#
|
|
15
16
|
# * `protocol_definition`
|
|
17
|
+
# * `supports_rpc?`
|
|
16
18
|
# * `on_shutdown`
|
|
17
19
|
# * `on_close`
|
|
20
|
+
# * `on_remote_disconnect`
|
|
18
21
|
#
|
|
19
22
|
class Rage::Cable::Protocols::Base
|
|
20
23
|
# @private
|
|
21
24
|
HANDSHAKE_HEADERS = {}
|
|
22
25
|
|
|
23
26
|
class << self
|
|
24
|
-
|
|
25
|
-
def init(router)
|
|
26
|
-
@router = router
|
|
27
|
-
|
|
27
|
+
def init
|
|
28
28
|
# Hash<String(stream name) => Set<Hash>(subscription params)>
|
|
29
29
|
@subscription_identifiers = Hash.new { |hash, key| hash[key] = Set.new }
|
|
30
|
+
@sync_channel = "cable:#{protocol_id}:synchronize"
|
|
30
31
|
|
|
31
32
|
Iodine.on_state(:pre_start) do
|
|
32
33
|
# this is a fallback to synchronize subscription identifiers across different worker processes;
|
|
33
34
|
# we expect connections to be distributed among all workers, so this code will almost never be called;
|
|
34
35
|
# we also synchronize subscriptions with the master process so that the forks that are spun up instead
|
|
35
36
|
# of the crashed ones also had access to the identifiers;
|
|
36
|
-
Iodine.subscribe(
|
|
37
|
+
Iodine.subscribe(@sync_channel) do |_, subscription_msg|
|
|
37
38
|
stream_name, params = Rage::ParamsParser.json_parse(subscription_msg)
|
|
38
39
|
@subscription_identifiers[stream_name] << params
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
Iodine.on_state(:on_finish) do
|
|
43
|
-
Iodine.unsubscribe(
|
|
44
|
+
Iodine.unsubscribe(@sync_channel)
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -54,11 +55,11 @@ class Rage::Cable::Protocols::Base
|
|
|
54
55
|
# @param name [String] the stream name
|
|
55
56
|
# @param params [Hash] parameters associated with the client
|
|
56
57
|
def subscribe(connection, name, params)
|
|
57
|
-
connection.subscribe("cable:#{name}:#{stream_id(params)}")
|
|
58
|
+
connection.subscribe("cable:#{protocol_id}:#{name}:#{stream_id(params)}")
|
|
58
59
|
|
|
59
60
|
unless @subscription_identifiers[name].include?(params)
|
|
60
61
|
@subscription_identifiers[name] << params
|
|
61
|
-
::Iodine.publish(
|
|
62
|
+
::Iodine.publish(@sync_channel, [name, params].to_json)
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -68,7 +69,7 @@ class Rage::Cable::Protocols::Base
|
|
|
68
69
|
# @param name [String] the stream name
|
|
69
70
|
# @param params [Hash] parameters associated with the client
|
|
70
71
|
def unsubscribe(connection, name, params)
|
|
71
|
-
connection.unsubscribe("cable:#{name}:#{stream_id(params)}")
|
|
72
|
+
connection.unsubscribe("cable:#{protocol_id}:#{name}:#{stream_id(params)}")
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# Broadcast data to all clients connected to a stream.
|
|
@@ -77,7 +78,7 @@ class Rage::Cable::Protocols::Base
|
|
|
77
78
|
# @param data [Object] the data to send
|
|
78
79
|
def broadcast(name, data)
|
|
79
80
|
@subscription_identifiers[name].each do |params|
|
|
80
|
-
::Iodine.publish("cable:#{name}:#{stream_id(params)}", serialize(params, data))
|
|
81
|
+
::Iodine.publish("cable:#{protocol_id}:#{name}:#{stream_id(params)}", serialize(params, data))
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
|
|
@@ -88,6 +89,13 @@ class Rage::Cable::Protocols::Base
|
|
|
88
89
|
true
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
# Process the disconnect initiated by the server.
|
|
93
|
+
#
|
|
94
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
|
95
|
+
# @param options [Hash, nil] options passed to the protocol when disconnecting
|
|
96
|
+
def on_remote_disconnect(connection, options)
|
|
97
|
+
end
|
|
98
|
+
|
|
91
99
|
private
|
|
92
100
|
|
|
93
101
|
def stream_id(params)
|