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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73e373fc25e95467aa4a38003efafda30488596a75fb1ad7cd617891a6918962
4
- data.tar.gz: 4183890376650de1e9133bcbe08ed645e628610629745d20c729e7464cb9ecb3
3
+ metadata.gz: 3042d8b6494404e49b19c70a5ee464e0b8f35a932bd1d5eff76a8fae8bc0713f
4
+ data.tar.gz: 4d69ac24a0dbd116c3cea590ecaad949fed45abe379660ab820ed8b5cbd05eec
5
5
  SHA512:
6
- metadata.gz: 4ac0ef8e80416b82f0249675ae98eb973df2e419b12f32521c30933105ad17cef790620a8965eec7c810dc67313d6be4d39e10d05bf937483eeea154c6136088
7
- data.tar.gz: 10b35efd2ba54aabc8eddc446669e945b530f0f7078899b43d45f8450fd2cf9db6ba746c695b4d2386e300a1adc84d98a2b9a0bd2e86e30d8ecf2f1804dd223f
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
@@ -22,9 +22,9 @@ group :test do
22
22
  gem "connection_pool", "~> 2.0"
23
23
  gem "rbnacl"
24
24
  gem "domain_name"
25
- gem "websocket-client-simple"
26
25
  gem "prism"
27
26
  gem "redis-client"
28
27
  gem "appraisal", "~> 2.5"
29
28
  gem "blueprinter"
29
+ gem "websocket", "~> 1.2"
30
30
  end
@@ -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
- handler = __build_handler(__protocol)
43
- accept_response = [0, __protocol.protocol_definition, []]
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
- application.define_singleton_method(:__rage_app_name) { "Rage::Cable" }
59
- chain.define_singleton_method(:__rage_root_app) { application }
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, __protocol)
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.__router
74
- @__router ||= Router.new
92
+ def self.__protocols
93
+ @__protocols ||= []
75
94
  end
76
95
 
77
96
  # @private
78
- def self.__protocol
79
- @__protocol ||= Rage.config.cable.protocol.tap { |protocol| protocol.init(__router) }
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
- schedule_fiber(connection) { @protocol.on_open(connection) }
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
- schedule_fiber(connection) { @protocol.on_message(connection, data) }
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
- schedule_fiber(connection) { @protocol.on_close(connection) }
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 schedule_fiber(connection)
122
- Fiber.schedule do
123
- @log_processor.init_request_logger(connection.env)
124
- yield
125
- rescue => e
126
- log_error(e)
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
- __protocol.broadcast(stream, data)
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
@@ -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 Rage.cable.__protocol.supports_rpc?
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
- Rage.cable.__protocol.subscribe(@__connection, stream, @__params)
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
- Rage.cable.__protocol.unsubscribe(@__connection, stream, @__params)
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 = Rage.cable.__protocol.serialize(@__params, data)
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
- # @see Rage::Cable::Router
90
- def self.on_open(connection)
91
- accepted = @router.process_connection(connection)
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
- # @see Rage::Cable::Router
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 = @router.process_subscription(connection, identifier, params[:channel], params)
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
- @router.process_disconnection(connection)
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
- @router.process_message(connection, identifier, data["action"].to_sym, data)
139
+ router.process_message(connection, env, identifier, data["action"].to_sym, data)
137
140
 
138
141
  elsif command == COMMAND::MESSAGE
139
- @router.process_message(connection, identifier, :receive, data)
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
- # @see Rage::Cable::Router
152
- def self.on_close(connection)
153
- @router.process_disconnection(connection)
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
- # @param router [Rage::Cable::Router]
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("cable:synchronize") do |_, subscription_msg|
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("cable:synchronize")
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("cable:synchronize", [name, params].to_json)
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)