anycable-rails-core 1.5.3 → 1.6.2

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +10 -75
  4. data/lib/anycable/rails/action_cable_ext/channel.rb +20 -11
  5. data/lib/anycable/rails/action_cable_ext/remote_connections.rb +4 -14
  6. data/lib/anycable/rails/channel/presence.rb +36 -0
  7. data/lib/anycable/rails/channel.rb +9 -0
  8. data/lib/anycable/rails/compatibility/rubocop.rb +2 -25
  9. data/lib/anycable/rails/compatibility.rb +2 -0
  10. data/lib/anycable/rails/config.rb +2 -0
  11. data/lib/anycable/rails/connection.rb +62 -33
  12. data/lib/anycable/rails/connection_factory.rb +20 -1
  13. data/lib/anycable/rails/connections/persistent_session.rb +5 -0
  14. data/lib/anycable/rails/connections/serializable_identification.rb +19 -0
  15. data/lib/anycable/rails/connections/session_proxy.rb +2 -2
  16. data/lib/anycable/rails/ext/jwt.rb +1 -1
  17. data/lib/anycable/rails/ext/signed_streams.rb +21 -0
  18. data/lib/anycable/rails/ext/whisper.rb +34 -0
  19. data/lib/anycable/rails/ext.rb +4 -1
  20. data/lib/anycable/rails/helper.rb +2 -2
  21. data/lib/anycable/rails/next/action_cable_ext/channel.rb +36 -0
  22. data/lib/anycable/rails/next/action_cable_ext/connection.rb +29 -0
  23. data/lib/anycable/rails/next/connection/persistent_session.rb +39 -0
  24. data/lib/anycable/rails/next/connection.rb +226 -0
  25. data/lib/anycable/rails/railtie.rb +4 -8
  26. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/instance_vars.rb +1 -1
  27. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/periodical_timers.rb +3 -8
  28. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/stream_from.rb +3 -5
  29. data/lib/anycable/rails/rubocop.rb +27 -0
  30. data/lib/anycable/rails/version.rb +1 -1
  31. data/lib/anycable/rails.rb +2 -1
  32. data/lib/generators/anycable/bin/templates/bin/anycable-go.tt +5 -0
  33. data/lib/generators/anycable/download/download_generator.rb +16 -5
  34. data/lib/generators/anycable/setup/setup_generator.rb +280 -38
  35. data/lib/generators/anycable/setup/templates/Procfile.dev.tt +2 -2
  36. data/lib/generators/anycable/setup/templates/anycable.toml.tt +81 -0
  37. data/lib/generators/anycable/setup/templates/bin/anycable-go.tt +5 -0
  38. data/lib/generators/anycable/setup/templates/bin/dev +17 -0
  39. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +20 -33
  40. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +2 -5
  41. metadata +27 -16
  42. data/lib/anycable/rails/action_cable_ext/signed_streams.rb +0 -31
  43. /data/lib/anycable/rails/{compatibility/rubocop → rubocop}/config/default.yml +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae33f5aaf1057a1aa63e16280048477ed7f6be5ff403e7a45bd4198aed3ef226
4
- data.tar.gz: dfedd48dbeddaf2e722093bec52f459c576c59a3c549e26afe00753ee770496b
3
+ metadata.gz: ae5f5e00ae334a317de5b59685ff57de668c2f102125f48bc8309a7330787950
4
+ data.tar.gz: f15f057412a3c0a0dbf759bd8c8d47abee1bee60a9b68ca62983ced577ea9397
5
5
  SHA512:
6
- metadata.gz: 3e84c51f6fb7b2c7e7a1b1535b2f99b4db0d83ee3235b45104aad07e353924bf2ce91269b4dbb9b992f356a622594f3c2e44941edf60b47be899196ee8adfd83
7
- data.tar.gz: 6b9b15397056c29d3cbf01b566ea4282e41dc2065f8af827bb73f102c47e78d429aecbfd11d687660eb4c820225fa3411d9bd7bd4fb39a75155568a0e4e24e05
6
+ metadata.gz: 8b780c7ca124e8f84d708e94ec78a27b50e9b36cbf524264cea3582bb171b037e40f4f92d8292f115810b988dfe93d45a942c779550943484bfad9810c1865f3
7
+ data.tar.gz: 916571c822234a92ec9bbb1839c06c8a0cc9a6f434c370c0b9bccd6486a450c2dc2ec88e27069d3b14bff093e245d0a83e6aed71fc7bcd1f20b298ae479edde7
data/CHANGELOG.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # Change log
2
2
 
3
- ## master
3
+ ## main
4
+
5
+ ## 1.6.2 (2026-04-02)
6
+
7
+ - Upgrade setup generator. ([@palkan][])
8
+
9
+ ## 1.6.1 (2025-12-18)
10
+
11
+ - Ensure `bin/dist/anycable-go` has execution permission as part of `bin/anycable-go` ([@dmorgan-fa][])
12
+
13
+ ## 1.6.0 (2025-03-18)
14
+
15
+ - Update `anycable:download` generator to support v1.6+. ([@palkan][])
16
+
17
+ - Add `#anycable_token_meta_tag` helper. ([@palkan][])
18
+
19
+ It generates a meta tag with the AnyCable JWT token (w/o the URL).
20
+
21
+ - Add Presence API. ([@palkan][])
22
+
23
+ ## 1.5.5 (2024-12-12)
24
+
25
+ - Publish RuboCop cops as a separate gem (`rubocop-anycable-rails`). ([@palkan][])
26
+
27
+ - Upgrade RuboCop cops. ([@palkan][])
28
+
29
+ ## 1.5.4 (2024-10-08)
30
+
31
+ - Add [actioncable-next](https://github.com/anycable/actioncable-next) support. ([@palkan][])
32
+
33
+ - Generate `anycable.toml` in `anycable:setup` generator. ([@palkan][])
4
34
 
5
35
  ## 1.5.3 (2024-09-12)
6
36
 
data/README.md CHANGED
@@ -1,97 +1,32 @@
1
- [![Gem Version](https://badge.fury.io/rb/anycable-rails.svg)](https://rubygems.org/gems/anycable-rails)
1
+ [![Gem Version](https://badge.fury.io/rb/anycable.svg)](https://rubygems.org/gems/anycable)
2
2
  [![Build](https://github.com/anycable/anycable-rails/workflows/Build/badge.svg)](https://github.com/anycable/anycable-rails/actions)
3
3
  [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://docs.anycable.io/rails/getting_started)
4
4
 
5
- # AnyCable Rails
5
+ # AnyCable SDK for Ruby on Rails
6
6
 
7
- AnyCable allows you to use any WebSocket server (written in any language) as a replacement for built-in Rails Action Cable server.
7
+ <img align="right" height="150" width="129"
8
+ title="AnyCable logo" src="https://docs.anycable.io/assets/images/logo.svg">
8
9
 
9
- With AnyCable you can use channels, client-side JS, broadcasting - (almost) all that you can do with Action Cable.
10
+ [AnyCable](https://github.com/anycable/anycable) is an open-source language-agnostic realtime server for reliable two-way communication over WebSockets and SSE.
11
+ This repository contains code for AnyCable Rails SKD that allows you to use AnyCable as a drop-in replacement for Action Cable.
10
12
 
11
- You can even use Action Cable in development and not be afraid of [compatibility issues](#compatibility).
13
+ 🌐 [Website](https://anycable.io) · 📚 [Documentation](https://docs.anycable.io/rails/getting_started) · 💾 [Example Rails Application](https://github.com/anycable/anycable_rails_demo)
12
14
 
13
- 💾 [Example Application](https://github.com/anycable/anycable_rails_demo)
14
-
15
- 📑 [Documentation](https://docs.anycable.io/rails/getting_started).
16
-
17
- > [AnyCable Pro](https://docs.anycable.io/pro) has been launched 🚀
18
-
19
- <a href="https://evilmartians.com/">
20
- <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
21
-
22
- ## Requirements
23
-
24
- - Ruby >= 2.6
25
- - Rails >= 6.0 (Rails 5.1 could work but we're no longer enforce compatibility on CI)
26
- - Redis (see [other options](https://github.com/anycable/anycable/issues/2) for broadcasting)
27
-
28
- ## Usage
15
+ ## Quick Start
29
16
 
30
17
  Add `anycable-rails` gem to your Gemfile:
31
18
 
32
19
  ```ruby
33
20
  gem "anycable-rails"
34
-
35
- # when using Redis broadcast adapter
36
- gem "redis", ">= 4.0"
37
21
  ```
38
22
 
39
- ### Interactive set up
40
-
41
- After the gem was installed, you can run an interactive wizard to configure your Rails application for using with AnyCable by running a generator:
23
+ Then run our interactive setup command:
42
24
 
43
25
  ```sh
44
26
  bundle exec rails g anycable:setup
45
27
  ```
46
28
 
47
- ### Manual set up
48
-
49
- Specify AnyCable subscription adapter for Action Cable:
50
-
51
- ```yml
52
- # config/cable.yml
53
- development:
54
- adapter: any_cable # or anycable
55
-
56
- production:
57
- adapter: any_cable
58
- ```
59
-
60
- and specify AnyCable WebSocket server URL:
61
-
62
- ```ruby
63
- # For development it's likely the localhost
64
-
65
- # config/environments/development.rb
66
- config.action_cable.url = "ws://localhost:8080/cable"
67
-
68
- # For production it's likely to have a sub-domain and secure connection
69
-
70
- # config/environments/production.rb
71
- config.action_cable.url = "wss://ws.example.com/cable"
72
- ```
73
-
74
- Then, run AnyCable RPC server:
75
-
76
- ```sh
77
- $ bundle exec anycable
78
-
79
- # don't forget to provide Rails env
80
-
81
- $ RAILS_ENV=production bundle exec anycable
82
- ```
83
-
84
- And, finally, run AnyCable WebSocket server, e.g. [anycable-go](https://docs.anycable.io/anycable-go/getting_started):
85
-
86
- ```sh
87
- anycable-go --host=localhost --port=8080
88
- ```
89
-
90
- See [documentation](https://docs.anycable.io/rails/getting_started) for more information on AnyCable + Rails usage.
91
-
92
- ## Action Cable Compatibility
93
-
94
- See [documentation](https://docs.anycable.io/rails/compatibility).
29
+ Learn more [here](https://docs.anycable.io/rails/getting_started).
95
30
 
96
31
  ## Contributing
97
32
 
@@ -3,17 +3,6 @@
3
3
  require "action_cable"
4
4
 
5
5
  ActionCable::Channel::Base.prepend(Module.new do
6
- def subscribe_to_channel
7
- super unless anycabled? && !@__anycable_subscribing__
8
- end
9
-
10
- def handle_subscribe
11
- @__anycable_subscribing__ = true
12
- subscribe_to_channel
13
- ensure
14
- @__anycable_subscribing__ = false
15
- end
16
-
17
6
  def start_periodic_timers
18
7
  super unless anycabled?
19
8
  end
@@ -57,3 +46,23 @@ ActionCable::Channel::Base.prepend(Module.new do
57
46
  connection.instance_variable_defined?(:@anycable_socket)
58
47
  end
59
48
  end)
49
+
50
+ # Handle $pubsub channel in Subscriptions
51
+ ActionCable::Connection::Subscriptions.prepend(Module.new do
52
+ # The contents are mostly copied from the original,
53
+ # there is no good way to configure channels mapping due to #safe_constantize
54
+ # and the layers of JSON
55
+ # https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/connection/subscriptions.rb
56
+ def add(data)
57
+ id_key = data["identifier"]
58
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
59
+
60
+ return if subscriptions.key?(id_key)
61
+
62
+ return super unless id_options[:channel] == "$pubsub"
63
+
64
+ subscription = AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
65
+ subscriptions[id_key] = subscription
66
+ subscription.subscribe_to_channel
67
+ end
68
+ end)
@@ -3,20 +3,10 @@
3
3
  require "action_cable/remote_connections"
4
4
 
5
5
  ActionCable::RemoteConnections::RemoteConnection.include(AnyCable::Rails::Connections::SerializableIdentification)
6
-
7
6
  ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
8
- # Rails <7.1 has no `reconnect` argument
9
- if ActionCable::RemoteConnections::RemoteConnection.instance_method(:disconnect).arity == 0
10
- def disconnect(reconnect: true)
11
- # Legacy Action Cable functionality if case we're not fully migrated yet
12
- super() unless AnyCable::Rails.enabled?
13
- ::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
14
- end
15
- else
16
- def disconnect(reconnect: true)
17
- # Legacy Action Cable functionality if case we're not fully migrated yet
18
- super unless AnyCable::Rails.enabled?
19
- ::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
20
- end
7
+ def disconnect(reconnect: true)
8
+ # Legacy Action Cable functionality if case we're not fully migrated yet
9
+ return super unless AnyCable::Rails.enabled?
10
+ ::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
21
11
  end
22
12
  end)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Channel
6
+ # Presence API for Action Cable channels (backed by AnyCable)
7
+ module Presence
8
+ extend ActiveSupport::Concern
9
+
10
+ def join_presence(stream = nil, id: user_presence_id, info: user_presence_info)
11
+ return unless anycabled?
12
+
13
+ stream ||= connection.anycable_socket.streams[:start].first || raise(ArgumentError, "Provide a stream name for presence updates")
14
+
15
+ connection.anycable_socket.presence_join(stream, id.to_s, info)
16
+ end
17
+
18
+ def leave_presence(id = user_presence_id)
19
+ return unless anycabled?
20
+
21
+ connection.anycable_socket.presence_leave(id)
22
+ end
23
+
24
+ private
25
+
26
+ def user_presence_id
27
+ connection.connection_identifier
28
+ end
29
+
30
+ def user_presence_info
31
+ # nothing
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Channel
6
+ autoload :Presence, "anycable/rails/channel/presence"
7
+ end
8
+ end
9
+ end
@@ -1,27 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
- require "pathname"
5
-
6
- require_relative "rubocop/cops/anycable/stream_from"
7
- require_relative "rubocop/cops/anycable/periodical_timers"
8
- require_relative "rubocop/cops/anycable/instance_vars"
9
-
10
- module RuboCop
11
- module AnyCable # :nodoc:
12
- CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
13
-
14
- # Merge anycable config into default configuration
15
- # See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
16
- def self.inject!
17
- path = CONFIG_DEFAULT.to_s
18
- puts "configuration from #{path}" if ConfigLoader.debug?
19
- hash = ConfigLoader.send(:load_yaml_configuration, path)
20
- config = Config.new(hash, path)
21
- config = ConfigLoader.merge_with_default(config, path)
22
- ConfigLoader.instance_variable_set(:@default_configuration, config)
23
- end
24
- end
25
- end
26
-
27
- RuboCop::AnyCable.inject!
3
+ # For backwards compatibility
4
+ require_relative "../rubocop"
@@ -8,6 +8,8 @@ module AnyCable
8
8
  @active_periodic_timers
9
9
  @_streams
10
10
  @parameter_filter
11
+ @whisper_stream
12
+ @__sid__
11
13
  ]
12
14
 
13
15
  ActionCable::Channel::Base.prepend(Module.new do
@@ -9,6 +9,7 @@ require "anyway/rails"
9
9
  # - `access_logs_disabled` (defaults to true) — whether to print Started/Finished logs
10
10
  # - `persistent_session_enabled` (defaults to false) — whether to store session changes in the connection state
11
11
  # - `embedded` (defaults to false) — whether to run RPC server inside a Rails server process
12
+ # - `http_rpc` (default to false) - whether to mount HTTP RPC server or not
12
13
  # - `http_rpc_mount_path` (defaults to nil) — path to mount HTTP RPC server
13
14
  # - `batch_broadcasts` (defaults to false) — whether to batch broadcasts automatically for code wrapped with Rails executor
14
15
  # - `jwt_param` (defaults to 'jid') — the name of the JWT authentication query paramter or header
@@ -18,6 +19,7 @@ AnyCable::Config.attr_config(
18
19
  persistent_session_enabled: false,
19
20
  embedded: false,
20
21
  jwt_param: "jid",
22
+ http_rpc: false,
21
23
  http_rpc_mount_path: nil,
22
24
  batch_broadcasts: false,
23
25
  socket_id_header: "X-Socket-ID",
@@ -31,15 +31,69 @@ module AnyCable
31
31
  end
32
32
 
33
33
  refine ActionCable::Connection::Subscriptions do
34
- # Find or add a subscription to the list
35
- def fetch(identifier)
36
- add("identifier" => identifier) unless subscriptions[identifier]
34
+ # Override the original #execute_command to pre-initialize the channel for unsubscribe/message and
35
+ # return true/false to indicate successful/unsuccessful subscription.
36
+ # We also must not lose any exceptions raised in the process.
37
+ def execute_rpc_command(data)
38
+ # First, verify the channel name
39
+ raise "Channel not found: #{ActiveSupport::JSON.decode(data["identifier"]).fetch("channel")}" unless subscription_class_from_identifier(data["identifier"])
40
+
41
+ if data["command"] == "subscribe"
42
+ add data
43
+ subscription = subscriptions[data["identifier"]]
44
+ return !(subscription.nil? || subscription.rejected?)
45
+ end
46
+
47
+ load(data["identifier"])
48
+
49
+ case data["command"]
50
+ when "unsubscribe"
51
+ remove data
52
+ when "message"
53
+ perform_action data
54
+ when "whisper"
55
+ whisper data
56
+ else
57
+ raise UnknownCommandError, data["command"]
58
+ end
37
59
 
38
- unless subscriptions[identifier]
39
- raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
60
+ true
61
+ end
62
+
63
+ # Restore channels from the list of identifiers and the state
64
+ def restore(subscriptions, istate)
65
+ subscriptions.each do |id|
66
+ channel = load(id)
67
+ channel.__istate__ = ActiveSupport::JSON.decode(istate[id]) if istate[id]
40
68
  end
69
+ end
70
+
71
+ # Find or create a channel for a given identifier
72
+ def load(identifier)
73
+ return subscriptions[identifier] if subscriptions[identifier]
41
74
 
42
- subscriptions[identifier]
75
+ subscription = subscription_from_identifier(identifier)
76
+ raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}" unless subscription
77
+
78
+ subscriptions[identifier] = subscription
79
+ end
80
+
81
+ def subscription_class_from_identifier(id_key)
82
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
83
+ if id_options[:channel] == "$pubsub"
84
+ PubSubChannel
85
+ else
86
+ id_options[:channel].safe_constantize
87
+ end
88
+ end
89
+
90
+ def subscription_from_identifier(id_key)
91
+ subscription_klass = subscription_class_from_identifier(id_key)
92
+
93
+ if subscription_klass && subscription_klass < ActionCable::Channel::Base
94
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
95
+ subscription_klass.new(connection, id_key, id_options)
96
+ end
43
97
  end
44
98
  end
45
99
  end)
@@ -74,15 +128,8 @@ module AnyCable
74
128
  conn.cached_ids = {}
75
129
  conn.anycable_request_builder = self
76
130
 
77
- return unless subscriptions
78
-
79
131
  # Pre-initialize channels (for disconnect)
80
- subscriptions.each do |id|
81
- channel = conn.subscriptions.fetch(id)
82
- next unless socket.istate[id]
83
-
84
- channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
85
- end
132
+ conn.subscriptions.restore(subscriptions, socket.istate) if subscriptions
86
133
  end
87
134
 
88
135
  def handle_open
@@ -111,26 +158,8 @@ module AnyCable
111
158
 
112
159
  def handle_channel_command(identifier, command, data)
113
160
  conn.run_callbacks :command do
114
- # We cannot use subscriptions#execute_command here,
115
- # since we MUST return true of false, depending on the status
116
- # of execution
117
- channel = conn.subscriptions.fetch(identifier)
118
- case command
119
- when "subscribe"
120
- channel.handle_subscribe
121
- !channel.rejected?
122
- when "unsubscribe"
123
- conn.subscriptions.remove_subscription(channel)
124
- true
125
- when "message"
126
- channel.perform_action ActiveSupport::JSON.decode(data)
127
- true
128
- else
129
- false
130
- end
161
+ conn.subscriptions.execute_rpc_command({"command" => command, "identifier" => identifier, "data" => data})
131
162
  end
132
- # Support rescue_from
133
- # https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
134
163
  rescue Exception => e # rubocop:disable Lint/RescueException
135
164
  rescue_with_handler(e) || raise
136
165
  false
@@ -1,6 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "anycable/rails/connection"
3
+ require "action_cable"
4
+
5
+ begin
6
+ ActionCable::Server::Socket
7
+ rescue
8
+ end
9
+
10
+ if defined?(ActionCable::Server::Socket)
11
+ require "anycable/rails/next/connection"
12
+ require "anycable/rails/next/action_cable_ext/connection"
13
+ require "anycable/rails/next/action_cable_ext/channel"
14
+ else
15
+ require "anycable/rails/connection"
16
+
17
+ require "anycable/rails/action_cable_ext/connection"
18
+ require "anycable/rails/action_cable_ext/channel"
19
+ end
20
+
21
+ require "anycable/rails/action_cable_ext/remote_connections"
22
+ require "anycable/rails/action_cable_ext/broadcast_options"
4
23
 
5
24
  module AnyCable
6
25
  module Rails
@@ -2,6 +2,11 @@
2
2
 
3
3
  require "anycable/rails/connections/session_proxy"
4
4
 
5
+ if defined?(ActionCable::Server::Socket)
6
+ require "anycable/rails/next/connection/persistent_session"
7
+ return
8
+ end
9
+
5
10
  module AnyCable
6
11
  module Rails
7
12
  module Connections
@@ -6,6 +6,20 @@ module AnyCable
6
6
  module SerializableIdentification
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ module ConnectionGID
10
+ def connection_identifier
11
+ unless defined? @connection_identifier
12
+ @connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get(:"@#{id}") || __send__(id) }
13
+ end
14
+
15
+ @connection_identifier
16
+ end
17
+ end
18
+
19
+ included do
20
+ prepend ConnectionGID
21
+ end
22
+
9
23
  class_methods do
10
24
  def identified_by(*identifiers)
11
25
  super
@@ -32,6 +46,11 @@ module AnyCable
32
46
  identifiers_hash.to_json
33
47
  end
34
48
 
49
+ def identifiers_json=(val)
50
+ @cached_ids = {}
51
+ @serialized_ids = val ? ActiveSupport::JSON.decode(val) : {}
52
+ end
53
+
35
54
  # Fetch identifier and deserialize if neccessary
36
55
  def fetch_identifier(name)
37
56
  return unless @cached_ids
@@ -46,9 +46,9 @@ module AnyCable
46
46
  rack_session.respond_to?(name, include_private) || super
47
47
  end
48
48
 
49
- def method_missing(method, *args, &block)
49
+ def method_missing(method, ...)
50
50
  if rack_session.respond_to?(method, true)
51
- rack_session.send(method, *args, &block)
51
+ rack_session.send(method, ...)
52
52
  else
53
53
  super
54
54
  end
@@ -10,7 +10,7 @@ module AnyCable
10
10
  super
11
11
  rescue AnyCable::JWT::ExpiredSignature
12
12
  logger.error "An expired JWT token was rejected"
13
- close(reason: "token_expired", reconnect: false) if websocket&.alive?
13
+ close(reason: "token_expired", reconnect: false)
14
14
  end
15
15
 
16
16
  def anycable_jwt_present?
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ ActionCable::Connection::Base.include(Module.new do
6
+ # This method is assumed to be overriden in the connection class to enable public
7
+ # streams
8
+ def allow_public_streams?
9
+ false
10
+ end
11
+ end)
12
+
13
+ # Handle $pubsub channel in Subscriptions
14
+ ActionCable::Connection::Subscriptions.prepend(Module.new do
15
+ def subscription_from_identifier(id_key)
16
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
17
+ return super unless id_options[:channel] == "$pubsub"
18
+
19
+ AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
20
+ end
21
+ end)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ ActionCable::Channel::Base.prepend(Module.new do
6
+ attr_accessor :whisper_stream
7
+
8
+ def stream_from(broadcasting, _callback = nil, **opts)
9
+ whispering = opts.delete(:whisper)
10
+ whispers_to(broadcasting) if whispering
11
+ super
12
+ end
13
+
14
+ def whispers_to(broadcasting)
15
+ logger.debug "#{self.class.name} whispers to #{broadcasting}"
16
+ self.whisper_stream = broadcasting
17
+ end
18
+ end)
19
+
20
+ ActionCable::Connection::Subscriptions.prepend(Module.new do
21
+ def execute_command(data)
22
+ return whisper(data) if data["command"] == "whisper"
23
+
24
+ super
25
+ end
26
+
27
+ def whisper(data)
28
+ subscription = find(data)
29
+ stream = subscription.whisper_stream
30
+ raise "Whispering stream is not set" unless stream
31
+
32
+ ::ActionCable.server.broadcast stream, data["data"]
33
+ end
34
+ end)
@@ -4,7 +4,10 @@ module AnyCable
4
4
  module Rails
5
5
  module Ext
6
6
  autoload :JWT, "anycable/rails/ext/jwt"
7
- autoload :SignedStreams, "anycable/rails/ext/signed_streams"
7
+
8
+ # These features are included by default
9
+ require "anycable/rails/ext/signed_streams"
10
+ require "anycable/rails/ext/whisper"
8
11
  end
9
12
  end
10
13
  end
@@ -22,10 +22,10 @@ module AnyCable
22
22
  tag "meta", name: "action-cable-url", content: url
23
23
  end
24
24
 
25
- def any_cable_jwt_meta_tag(**identifiers)
25
+ def anycable_token_meta_tag(**identifiers)
26
26
  token = JWT.encode(identifiers)
27
27
 
28
- tag "meta", name: "any-cable-jwt", content: token
28
+ tag "meta", name: "cable-token", content: token
29
29
  end
30
30
 
31
31
  def signed_stream_name(streamables)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ ActionCable::Channel::Base.prepend(Module.new do
6
+ # Whispering support
7
+ def whispers_to(broadcasting)
8
+ return super unless anycabled?
9
+
10
+ connection.anycable_socket.whisper identifier, broadcasting
11
+ end
12
+
13
+ # Unsubscribing relies on the channel state (which is not persistent in AnyCable).
14
+ # Thus, we pretend that the stream is registered to make Action Cable do its unsubscribing job.
15
+ def stop_stream_from(broadcasting)
16
+ streams[broadcasting] = true if anycabled?
17
+ super
18
+ end
19
+
20
+ # For AnyCable, unsubscribing from all streams is a separate operation,
21
+ # so we use a special constant to indicate it.
22
+ def stop_all_streams
23
+ if anycabled?
24
+ streams.clear
25
+ streams[AnyCable::Rails::Server::PubSub::ALL_STREAMS] = true
26
+ end
27
+ super
28
+ end
29
+
30
+ # Make rejected status accessible from outside
31
+ def rejected? = subscription_rejected?
32
+
33
+ private
34
+
35
+ def anycabled? = connection.anycabled?
36
+ end)