anycable-rails-core 1.4.4 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62bc4f7d0aef85236e0ff6a983e2a8c2db681397a70dacc91c85bbd4662377e8
4
- data.tar.gz: 20cae16d638d1c47a96d5b235350c156d5e98f597858f72c9acab35aba3b0f0f
3
+ metadata.gz: e23407a3fe0b10d54fdb4c0b368f23df917dc5f901b821312c9464c37099c23d
4
+ data.tar.gz: ce1ea7b919210c7ea6a2d0098a5bf90141ce3910d57f17f9cbb116b96d93753f
5
5
  SHA512:
6
- metadata.gz: bbb7e9670097c6ee0ce230f1a8226ffe0449c2cf4556b84bf465638fef385140c0aadbf2378477e37d09969617e74c66acda8c3c1f0075280abdc1a32792da58
7
- data.tar.gz: 3a5498a417a077468f7adc6638c31f7d92a304f541475957bc74233e674c9cc6fc010ce432c9699b0eba2d308c8c1aacb4d85a21a7da7f48f8773162ece3c3fd
6
+ metadata.gz: 658fffc579a1ddea3001a6ed05f6b59dbe1f05f48ce27c70924cd0ae18b0d8c2bd4c3151b0ea82b52ea78f37e9217f2b80769683dadeba21b2f9f50b6588a566
7
+ data.tar.gz: c8982b7cae01bfdefa4c0bef71581b7de2b579a0471bf2ff32619c043e84710822026e9814682474e84c386880808d79a651db0d42ff68d0d7ae646630050522
data/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.5.0 (2024-04-01)
6
+
7
+ - Allow specifying the _whispering_ stream via `#stream_from(..., whisper: true)`. ([@palkan][])
8
+
9
+ You can use specify the stream to use for _whispering_ (client-initiated broadcasts) by adding `whisper: true` to the `#stream_from` (or `#stream_for`) method call.
10
+
11
+ NOTE: This feature is only supported when using AnyCable server and ignored otherwise.
12
+
13
+ - Support passing objects to `ActionCable.server.broadcast`. ([@palkan][])
14
+
15
+ Make it possible to call `ActionCable.server.broadcast(user, data)` or `ActionCable.server.broadcast([user, :context], data)`. This is a companion functionality for `#signed_stream_name`.
16
+
17
+ - Added `websocket_url` configuration option to specify the URL of AnyCable server. ([@palkan][])
18
+
19
+ It's used to configure `config.action_cable.url` if AnyCable is activated. No need to set up it manually.
20
+
21
+ - Added `rails g anycable:bin` to create a binstub to run AnyCable server. ([@palkan][])
22
+
23
+ - Added signed streams helpers. ([@palkan][])
24
+
25
+ You can use `#signed_stream_name(streamable)` to generate a signed stream name.
26
+
27
+ - Added JWT authentication helpers. ([@palkan][])
28
+
29
+ No more need in a separate `anycable-rails-jwt` gem.
30
+
31
+ ## 1.4.5
32
+
33
+ - Restrict `anycable` gem version.
34
+
5
35
  ## 1.4.4 (2024-03-08) 🌷
6
36
 
7
37
  - Minor fixes
@@ -8,7 +8,7 @@ module ActionCable
8
8
  # to AnyCable
9
9
  class AnyCable < Base
10
10
  ACTION_CABLE_SERVER_ERROR_MESSAGE = <<~STR
11
- Looks like you're trying to connect to Rails Action Cable server, not an AnyCable one.
11
+ Looks like you are trying to connect to Rails Action Cable server, not an AnyCable one.
12
12
 
13
13
  Please make sure your client is configured to connect to AnyCable server.
14
14
 
@@ -10,6 +10,11 @@ ActionCable::Server::Base.prepend(Module.new do
10
10
  super(channel, payload)
11
11
  end
12
12
  end
13
+
14
+ def broadcaster_for(broadcasting, **options)
15
+ broadcasting = AnyCable::Rails.stream_name_from(broadcasting)
16
+ super(broadcasting, **options)
17
+ end
13
18
  end)
14
19
 
15
20
  ActionCable::Channel::Base.singleton_class.prepend(Module.new do
@@ -22,12 +22,20 @@ ActionCable::Channel::Base.prepend(Module.new do
22
22
  super unless anycabled?
23
23
  end
24
24
 
25
- def stream_from(broadcasting, _callback = nil, **)
25
+ def stream_from(broadcasting, _callback = nil, **opts)
26
+ whispering = opts.delete(:whisper)
26
27
  return super unless anycabled?
27
28
 
28
29
  broadcasting = String(broadcasting)
29
30
 
30
31
  connection.anycable_socket.subscribe identifier, broadcasting
32
+ if whispering
33
+ connection.anycable_socket.whisper identifier, broadcasting
34
+ end
35
+ end
36
+
37
+ def stream_for(model, callback = nil, **opts, &block)
38
+ stream_from(broadcasting_for(model), callback || block, **opts)
31
39
  end
32
40
 
33
41
  def stop_stream_from(broadcasting)
@@ -0,0 +1,31 @@
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
+ # The contents are mostly copied from the original,
16
+ # there is no good way to configure channels mapping due to #safe_constantize
17
+ # and the layers of JSON
18
+ # https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/connection/subscriptions.rb
19
+ def add(data)
20
+ id_key = data["identifier"]
21
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
22
+
23
+ return if subscriptions.key?(id_key)
24
+
25
+ return super unless id_options[:channel] == "$pubsub"
26
+
27
+ subscription = AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
28
+ subscriptions[id_key] = subscription
29
+ subscription.subscribe_to_channel
30
+ end
31
+ end)
@@ -10,11 +10,16 @@ module AnyCable
10
10
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
11
11
  def #{name}
12
12
  return @#{name} if instance_variable_defined?(:@#{name})
13
- @#{name} = AnyCable::Rails.deserialize(__istate__["#{name}"], json: true) if anycabled?
13
+ return unless anycabled?
14
+
15
+ raw_val = __istate__["#{name}"]
16
+ val = raw_val.present? ? AnyCable::Serializer.deserialize(JSON.parse(raw_val)) : nil
17
+
18
+ @#{name} = val.try(:with_indifferent_access) || val
14
19
  end
15
20
 
16
21
  def #{name}=(val)
17
- __istate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
22
+ __istate__["#{name}"] = AnyCable::Serializer.serialize(val).to_json if anycabled?
18
23
  instance_variable_set(:@#{name}, val)
19
24
  end
20
25
  RUBY
@@ -53,11 +58,14 @@ module AnyCable
53
58
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
54
59
  def #{name}
55
60
  return @#{name} if instance_variable_defined?(:@#{name})
56
- @#{name} = AnyCable::Rails.deserialize(__cstate__["#{name}"], json: true) if anycabled?
61
+ return unless anycabled?
62
+
63
+ val = __cstate__["#{name}"]
64
+ @#{name} = val.present? ? AnyCable::Serializer.deserialize(JSON.parse(val)) : nil
57
65
  end
58
66
 
59
67
  def #{name}=(val)
60
- __cstate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
68
+ __cstate__["#{name}"] = AnyCable::Serializer.serialize(val).to_json if anycabled?
61
69
  instance_variable_set(:@#{name}, val)
62
70
  end
63
71
  RUBY
@@ -11,7 +11,7 @@ module AnyCable
11
11
  ]
12
12
 
13
13
  ActionCable::Channel::Base.prepend(Module.new do
14
- def stream_from(broadcasting, callback = nil, coder: nil)
14
+ def stream_from(broadcasting, callback = nil, coder: nil, **)
15
15
  if coder.present? && coder != ActiveSupport::JSON
16
16
  raise AnyCable::CompatibilityError, "Custom coders are not supported by AnyCable"
17
17
  end
@@ -9,15 +9,19 @@ 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_mount_path` (default to nil) — path to mount HTTP RPC server
12
+ # - `http_rpc_mount_path` (defaults to nil) — path to mount HTTP RPC server
13
13
  # - `batch_broadcasts` (defaults to false) — whether to batch broadcasts automatically for code wrapped with Rails executor
14
+ # - `jwt_param` (defaults to 'jid') — the name of the JWT authentication query paramter or header
15
+ # - `websocket_url` (defaults to nil) — the URL of AnyCable server WebSocket endpoint
14
16
  AnyCable::Config.attr_config(
15
17
  access_logs_disabled: true,
16
18
  persistent_session_enabled: false,
17
19
  embedded: false,
20
+ jwt_param: "jid",
18
21
  http_rpc_mount_path: nil,
19
22
  batch_broadcasts: false,
20
23
  socket_id_header: "X-Socket-ID",
21
- disable_rpc_pool_size_warning: false
24
+ disable_rpc_pool_size_warning: false,
25
+ websocket_url: nil
22
26
  )
23
27
  AnyCable::Config.ignore_options :access_logs_disabled, :persistent_session_enabled
@@ -24,7 +24,7 @@ module AnyCable
24
24
  obj = instance_variable_get("@#{id}")
25
25
  next unless obj
26
26
 
27
- acc[id] = AnyCable::Rails.serialize(obj)
27
+ acc[id] = AnyCable::Serializer.serialize(obj)
28
28
  end.compact
29
29
  end
30
30
 
@@ -37,7 +37,7 @@ module AnyCable
37
37
  return unless @cached_ids
38
38
 
39
39
  @cached_ids[name] ||= @cached_ids.fetch(name) do
40
- AnyCable::Rails.deserialize(@serialized_ids[name.to_s])
40
+ AnyCable::Serializer.deserialize(@serialized_ids[name.to_s])
41
41
  end
42
42
  end
43
43
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Ext
6
+ # This module adds AnyCable JWT helpers to Action Cable
7
+ module JWT
8
+ # Handle expired tokens here to respond with a different disconnect reason
9
+ def handle_open
10
+ super
11
+ rescue AnyCable::JWT::ExpiredSignature
12
+ logger.error "An expired JWT token was rejected"
13
+ close(reason: "token_expired", reconnect: false) if websocket&.alive?
14
+ end
15
+
16
+ def anycable_jwt_present?
17
+ request.params[AnyCable.config.jwt_param].present? ||
18
+ request.headers["x-#{AnyCable.config.jwt_param}"].present?
19
+ end
20
+
21
+ def identify_from_anycable_jwt!
22
+ token = request.params[AnyCable.config.jwt_param].presence ||
23
+ request.headers["x-#{AnyCable.config.jwt_param}"].presence
24
+
25
+ identifiers = AnyCable::JWT.decode(token)
26
+ identifiers.each do |k, v|
27
+ public_send("#{k}=", v)
28
+ end
29
+ rescue AnyCable::JWT::VerificationError
30
+ reject_unauthorized_connection
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Ext
6
+ autoload :JWT, "anycable/rails/ext/jwt"
7
+ autoload :SignedStreams, "anycable/rails/ext/signed_streams"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module AnyCable
6
+ module Rails
7
+ module Helper
8
+ def action_cable_with_jwt_meta_tag(**identifiers)
9
+ # From: https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/helpers/action_cable_helper.rb
10
+ base_url = ActionCable.server.config.url ||
11
+ ActionCable.server.config.mount_path ||
12
+ raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
13
+
14
+ token = JWT.encode(identifiers)
15
+
16
+ parts = [base_url, "#{AnyCable.config.jwt_param}=#{token}"]
17
+
18
+ uri = URI.parse(base_url)
19
+
20
+ url = parts.join(uri.query ? "&" : "?")
21
+
22
+ tag "meta", name: "action-cable-url", content: url
23
+ end
24
+
25
+ def any_cable_jwt_meta_tag(**identifiers)
26
+ token = JWT.encode(identifiers)
27
+
28
+ tag "meta", name: "any-cable-jwt", content: token
29
+ end
30
+
31
+ def signed_stream_name(streamables)
32
+ Rails.signed_stream_name(streamables)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module ObjectSerializer
6
+ module_function
7
+
8
+ # Serialize via GlobalID if available
9
+ def serialize(obj)
10
+ obj.try(:to_gid_param)
11
+ end
12
+
13
+ # Deserialize from GlobalID
14
+ def deserialize(str)
15
+ GlobalID::Locator.locate(str)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ class PubSubChannel < ActionCable::Channel::Base
6
+ def subscribed
7
+ stream_name =
8
+ if params[:stream_name] && connection.allow_public_streams?
9
+ params[:stream_name]
10
+ elsif params[:signed_stream_name]
11
+ AnyCable::Streams.verified(params[:signed_stream_name])
12
+ end
13
+
14
+ if stream_name
15
+ stream_from stream_name
16
+ else
17
+ reject
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -4,6 +4,7 @@ require "anycable/rails/action_cable_ext/connection"
4
4
  require "anycable/rails/action_cable_ext/channel"
5
5
  require "anycable/rails/action_cable_ext/remote_connections"
6
6
  require "anycable/rails/action_cable_ext/broadcast_options"
7
+ require "anycable/rails/action_cable_ext/signed_streams"
7
8
 
8
9
  require "anycable/rails/channel_state"
9
10
  require "anycable/rails/connection_factory"
@@ -17,6 +18,19 @@ module AnyCable
17
18
  app.config.action_cable.mount_path = nil
18
19
  end
19
20
 
21
+ initializer "anycable.websocket_url", after: "action_cable.set_configs" do |app|
22
+ next unless AnyCable::Rails.enabled?
23
+
24
+ websocket_url = AnyCable.config.websocket_url.presence
25
+ next unless websocket_url
26
+
27
+ app.config.action_cable.url = websocket_url
28
+
29
+ ActiveSupport.on_load(:action_cable) do
30
+ ::ActionCable.server.config.url = websocket_url
31
+ end
32
+ end
33
+
20
34
  initializer "anycable.logger", after: "action_cable.logger" do |_app|
21
35
  AnyCable.logger = ::ActionCable.server.config.logger
22
36
 
@@ -51,6 +65,11 @@ module AnyCable
51
65
  end
52
66
  end
53
67
 
68
+ initializer "anycable.object_serializer" do |_app|
69
+ require "anycable/rails/object_serializer"
70
+ AnyCable::Serializer.object_serializer = AnyCable::Rails::ObjectSerializer
71
+ end
72
+
54
73
  initializer "anycable.executor" do |app|
55
74
  require "anycable/rails/middlewares/executor"
56
75
  # see https://github.com/rails/rails/pull/33469/files
@@ -131,6 +150,12 @@ module AnyCable
131
150
  end
132
151
  end
133
152
 
153
+ initializer "anycable.helpers" do
154
+ ActiveSupport.on_load(:action_view) do
155
+ include AnyCable::Rails::Helper
156
+ end
157
+ end
158
+
134
159
  # Since Rails 6.1
135
160
  if respond_to?(:server)
136
161
  server do
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "1.4.4"
5
+ VERSION = "1.5.0"
6
6
  end
7
7
  end
@@ -8,9 +8,14 @@ require "anycable/rails/rack"
8
8
  require "globalid"
9
9
  require "active_support/core_ext/module/attribute_accessors_per_thread"
10
10
 
11
+ require "anycable/rails/ext"
12
+
11
13
  module AnyCable
12
14
  # Rails handler for AnyCable
13
15
  module Rails
16
+ autoload :Helper, "anycable/rails/helper"
17
+ autoload :PubSubChannel, "anycable/rails/pubsub_channel"
18
+
14
19
  require "anycable/rails/railtie"
15
20
 
16
21
  ADAPTER_ALIASES = %w[any_cable anycable].freeze
@@ -51,28 +56,15 @@ module AnyCable
51
56
  end
52
57
  end
53
58
 
54
- # Serialize connection/channel state variable to string
55
- # using GlobalID where possible or JSON (if json: true)
56
- def serialize(obj, json: false)
57
- obj.try(:to_gid_param) || (json ? obj.to_json : obj)
59
+ def signed_stream_name(streamables)
60
+ Streams.signed(stream_name_from(streamables))
58
61
  end
59
62
 
60
- # Deserialize previously serialized value from string to
61
- # Ruby object.
62
- # If the resulting object is a Hash, make it indifferent
63
- def deserialize(str, json: false)
64
- str.yield_self do |val|
65
- next val unless val.is_a?(String)
66
-
67
- gval = GlobalID::Locator.locate(val)
68
- return gval if gval
69
-
70
- next val unless json
71
-
72
- JSON.parse(val)
73
- end.yield_self do |val|
74
- next val.with_indifferent_access if val.is_a?(Hash)
75
- val
63
+ def stream_name_from(streamables)
64
+ if streamables.is_a?(Array)
65
+ streamables.map { |streamable| stream_name_from(streamable) }.join(":")
66
+ else
67
+ streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
76
68
  end
77
69
  end
78
70
 
@@ -0,0 +1,10 @@
1
+ Description:
2
+ Generate bin/anycable-go to automatially install and run AnyCable server locally
3
+
4
+ Example:
5
+ rails generate anycable:bin
6
+
7
+ This will create:
8
+ `bin/anycable-go`.
9
+
10
+ rails generate anycable:bin --version=1.4.0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "generators/anycable/with_os_helpers"
4
+
5
+ module AnyCableRailsGenerators
6
+ # Generates bin/anycable-go binstub
7
+ class BinGenerator < ::Rails::Generators::Base
8
+ namespace "anycable:bin"
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ class_option :version,
13
+ type: :string,
14
+ desc: "Specify AnyCable server version (defaults to latest release)",
15
+ version: "latest"
16
+
17
+ def generate_bin
18
+ inside("bin") do
19
+ template "anycable-go"
20
+ chmod "anycable-go", 0755, verbose: false # rubocop:disable Style/NumericLiteralPrefix
21
+ end
22
+
23
+ in_root do
24
+ next unless File.file?(".gitignore")
25
+
26
+ ignores = File.read(".gitignore").lines
27
+
28
+ if ignores.none? { |line| line.match?(/^bin\/dist$/) }
29
+ append_file ".gitignore", "bin/dist\n"
30
+ end
31
+ end
32
+
33
+ true
34
+ end
35
+
36
+ private
37
+
38
+ def anycable_go_version
39
+ @anycable_go_version ||= normalize_version(options[:version]) || "latest"
40
+ end
41
+
42
+ def normalize_version(version)
43
+ return unless version
44
+ return if version.chomp == "latest"
45
+
46
+ # We need a full version for bin/anycable-go script
47
+ segments = Gem::Version.new(version).segments
48
+ segments << 0 until segments.size >= 3
49
+ segments.join(".")
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+
3
+ cd $(dirname $0)/..
4
+
5
+ # It's recommended to use the exact version of AnyCable here
6
+ version="<%= anycable_go_version %>"
7
+
8
+ if [ ! -f ./bin/dist/anycable-go ]; then
9
+ echo "AnyCable server is not installed, downloading..."
10
+ ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist
11
+ fi
12
+
13
+ curVersion=$(./bin/dist/anycable-go -v)
14
+
15
+ if [[ "$version" != "latest" ]]; then
16
+ if [[ "$curVersion" != "$version"* ]]; then
17
+ echo "AnyCable server version is not $version, downloading a new one..."
18
+ ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist
19
+ fi
20
+ fi
21
+
22
+ ./bin/dist/anycable-go $@
@@ -1,5 +1,5 @@
1
1
  Description:
2
- Install AnyCable-Go web server locally (the latest version by default).
2
+ Install AnyCable real-time server locally (the latest version by default).
3
3
 
4
4
  Example:
5
5
  rails generate anycable:download
@@ -11,4 +11,4 @@ Example:
11
11
 
12
12
  rails generate anycable:download --bin-path=/usr/local/bin
13
13
 
14
- rails generate anycable:download --version=1.0.0
14
+ rails generate anycable:download --version=1.4.0
@@ -13,10 +13,10 @@ module AnyCableRailsGenerators
13
13
 
14
14
  class_option :bin_path,
15
15
  type: :string,
16
- desc: "Where to download AnyCable-Go server binary (default: #{DEFAULT_BIN_PATH})"
16
+ desc: "Where to download AnyCable server binary (default: #{DEFAULT_BIN_PATH})"
17
17
  class_option :version,
18
18
  type: :string,
19
- desc: "Specify the AnyCable-Go version (defaults to latest release)"
19
+ desc: "Specify the AnyCable version (defaults to latest release)"
20
20
 
21
21
  def download_bin
22
22
  out = options[:bin_path] || DEFAULT_BIN_PATH
@@ -74,7 +74,7 @@ module AnyCableRailsGenerators
74
74
  if yes? "Path is not writable 😕. Do you have sudo privileges?"
75
75
  sudo = "sudo "
76
76
  else
77
- say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
77
+ say_status :error, "❌ Failed to install AnyCable real-time server", :red
78
78
  raise StandardError, "Path #{path} is not writable!"
79
79
  end
80
80
  end
@@ -3,9 +3,3 @@ Description:
3
3
 
4
4
  Example:
5
5
  rails generate anycable:setup
6
-
7
- # You can opt-in to use HTTP RPC
8
- rails generate anycable:setup --rpc=http
9
-
10
- # You can specify which version of anycable-go to use
11
- rails generate anycable:setup --version=1.4
@@ -10,105 +10,63 @@ module AnyCableRailsGenerators
10
10
 
11
11
  DOCS_ROOT = "https://docs.anycable.io"
12
12
  DEVELOPMENT_METHODS = %w[skip local docker].freeze
13
- RPC_IMPL = %w[grpc http].freeze
13
+ DEPLOYMENT_METHODS = %w[other fly heroku anycable_plus].freeze
14
+ RPC_IMPL = %w[none grpc http].freeze
14
15
 
15
16
  class_option :devenv,
16
17
  type: :string,
17
- desc: "Select your development environment (options: #{DEVELOPMENT_METHODS.join(", ")})"
18
+ desc: "Select your development environment (options: #{DEVELOPMENT_METHODS.reverse.join(", ")})"
18
19
  class_option :rpc,
19
20
  type: :string,
20
- desc: "Select RPC implementation (options: #{RPC_IMPL.join(", ")})",
21
- default: "grpc"
22
- class_option :skip_heroku,
21
+ desc: "Select RPC implementation (options: #{RPC_IMPL.reverse.join(", ")})"
22
+ class_option :skip_procfile,
23
23
  type: :boolean,
24
- desc: "Do not copy Heroku configs"
25
- class_option :skip_procfile_dev,
26
- type: :boolean,
27
- desc: "Do not create Procfile.dev"
28
- class_option :skip_jwt,
29
- type: :boolean,
30
- desc: "Do not install anycable-rails-jwt"
31
- class_option :skip_install,
32
- type: :boolean,
33
- desc: "Do not run bundle install when adding new gems"
24
+ desc: "Do not create/update Procfile.dev"
34
25
  class_option :version,
35
26
  type: :string,
36
- desc: "Specify the AnyCable-Go version (defaults to latest release)"
27
+ desc: "Specify AnyCable server version (defaults to latest release)",
28
+ default: "latest"
37
29
 
38
30
  def welcome
39
- say "👋 Welcome to AnyCable interactive installer."
31
+ say ""
32
+ say "👋 Welcome to AnyCable interactive installer. We'll guide you through the process of installing AnyCable for your Rails application. Buckle up!"
33
+ say ""
40
34
  end
41
35
 
42
- def configs
43
- inside("config") do
44
- template "cable.yml"
45
- template "anycable.yml"
46
- end
47
- end
36
+ def rpc_implementation
37
+ say "AnyCable connects to your Rails server to communicate with Action Cable channels (via RPC API). Learn more from the docs 👉 #{DOCS_ROOT}/anycable-go/rpc"
38
+ say ""
48
39
 
49
- def cable_url
50
- environment(nil, env: :development) do
51
- <<~SNIPPET
52
- # Specify AnyCable WebSocket server URL to use by JS client
53
- config.after_initialize do
54
- config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "ws://localhost:8080/cable") if AnyCable::Rails.enabled?
55
- end
56
- SNIPPET
57
- end
40
+ answer = RPC_IMPL.index(options[:rpc]) || 99
58
41
 
59
- environment(nil, env: :production) do
60
- <<~SNIPPET
61
- # Specify AnyCable WebSocket server URL to use by JS client
62
- config.after_initialize do
63
- config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "/cable") if AnyCable::Rails.enabled?
64
- end
65
- SNIPPET
42
+ until RPC_IMPL[answer.to_i]
43
+ answer = ask "Do you want to use gRPC or HTTP for AnyCable RPC? (1) gRPC, (2) HTTP, (0) None"
66
44
  end
67
45
 
68
- say_status :info, "✅ 'config.action_cable.url' has been configured"
69
- say_status :help, "⚠️ If you're using JS client make sure you have " \
70
- "`action_cable_meta_tag` included before any <script> tag in your application.html"
46
+ @rpc_impl = RPC_IMPL[answer.to_i]
71
47
  end
72
48
 
73
49
  def development_method
74
50
  answer = DEVELOPMENT_METHODS.index(options[:devenv]) || 99
75
51
 
52
+ say ""
53
+
76
54
  until DEVELOPMENT_METHODS[answer.to_i]
77
- answer = ask "Do you want to run anycable-go locally or as a Docker container? (1) Local, (2) Docker, (0) Skip"
55
+ answer = ask "Do you want to run AnyCable server (anycable-go) locally or as a Docker container? (1) Local, (2) Docker, (0) Skip"
78
56
  end
79
57
 
80
- case env = DEVELOPMENT_METHODS[answer.to_i]
58
+ @devenv = DEVELOPMENT_METHODS[answer.to_i]
59
+
60
+ case @devenv
81
61
  when "skip"
82
- say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
62
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
83
63
  else
84
- send "install_for_#{env}"
64
+ send "install_for_#{@devenv}"
85
65
  end
86
66
  end
87
67
 
88
- def heroku
89
- if options[:skip_heroku].nil?
90
- return unless yes? "Do you use Heroku for deployment? [Yn]"
91
- elsif options[:skip_heroku]
92
- return
93
- end
94
-
95
- in_root do
96
- next unless File.file?("Procfile")
97
- next if http_rpc?
98
-
99
- contents = File.read("Procfile")
100
- contents.sub!(/^web: (.*)$/, %q(web: [[ "$ANYCABLE_DEPLOYMENT" == "true" ]] && bundle exec anycable --server-command="anycable-go" || \1))
101
- File.write("Procfile", contents)
102
- say_status :info, "✅ Procfile updated"
103
- end
104
-
105
- say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 #{DOCS_ROOT}/deployment/heroku", :yellow
106
- end
107
-
108
68
  def devise
109
- in_root do
110
- return unless File.file?("config/initializers/devise.rb")
111
- end
69
+ return unless devise?
112
70
 
113
71
  inside("config/initializers") do
114
72
  template "anycable.rb"
@@ -117,10 +75,12 @@ module AnyCableRailsGenerators
117
75
  say_status :info, "✅ config/initializers/anycable.rb with Devise configuration has been added"
118
76
  end
119
77
 
120
- def stimulus_reflex
121
- return unless stimulus_reflex?
78
+ def configs
79
+ inside("config") do
80
+ template "anycable.yml"
81
+ end
122
82
 
123
- say_status :help, "⚠️ Please, check out the documentation on using AnyCable with Stimulus Reflex: #{DOCS_ROOT}/rails/stimulus_reflex"
83
+ update_cable_yml
124
84
  end
125
85
 
126
86
  def rubocop_compatibility
@@ -131,14 +91,15 @@ module AnyCableRailsGenerators
131
91
  say_status :help, "⚠️ Please, take a look at the icompatibilities above and fix them. See #{DOCS_ROOT}/rails/compatibility" unless res
132
92
  end
133
93
 
134
- def jwt
135
- return if options[:skip_jwt]
136
-
137
- return unless options[:skip_jwt] == false || yes?("Do you want to use JWT for authentication? [Yn]")
94
+ def cable_url_info
95
+ say_status :help, "⚠️ If you're using JS client make sure you have " \
96
+ "`action_cable_meta_tag` or `action_cable_with_jwt_meta_tag` included in your HTML layout"
97
+ end
138
98
 
139
- opts = " --skip-install" if options[:skip_install]
99
+ def deployment_method
100
+ say_status :info, "🚢 See our deployment guide to learn how to run AnyCable in production 👉 #{DOCS_ROOT}/deployment"
140
101
 
141
- run "bundle add anycable-rails-jwt#{opts}"
102
+ say_status :info, "Check out AnyCable+, our hosted AnyCable solution: https://plus.anycable.io"
142
103
  end
143
104
 
144
105
  def finish
@@ -147,10 +108,6 @@ module AnyCableRailsGenerators
147
108
 
148
109
  private
149
110
 
150
- def stimulus_reflex?
151
- !!gemfile_lock&.match?(/^\s+stimulus_reflex\b/)
152
- end
153
-
154
111
  def redis?
155
112
  !!gemfile_lock&.match?(/^\s+redis\b/)
156
113
  end
@@ -167,8 +124,20 @@ module AnyCableRailsGenerators
167
124
  !!gemfile_lock&.match?(/^\s+rubocop\b/)
168
125
  end
169
126
 
127
+ def devise?
128
+ !!gemfile_lock&.match?(/^\s+devise\b/)
129
+ end
130
+
131
+ def local?
132
+ @devenv == "local"
133
+ end
134
+
135
+ def grpc?
136
+ @rpc_impl == "grpc"
137
+ end
138
+
170
139
  def http_rpc?
171
- options[:rpc] == "http"
140
+ @rpc_impl == "http"
172
141
  end
173
142
 
174
143
  def gemfile_lock
@@ -183,83 +152,143 @@ module AnyCableRailsGenerators
183
152
  end
184
153
 
185
154
  def install_for_docker
186
- # Remove localhost from configuraiton
187
- gsub_file "config/anycable.yml", /^.*redis_url:.*localhost[^\n]+\n/, ""
188
-
189
155
  say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
190
156
 
191
- say "Here is an example snippet for docker-compose.yml:"
192
- say <<~YML
193
- ─────────────────────────────────────────
194
- ws:
195
- image: anycable/anycable-go:1.4
196
- ports:
197
- - '8080:8080'
198
- environment:
199
- ANYCABLE_HOST: "0.0.0.0"
200
- ANYCABLE_REDIS_URL: redis://redis:6379/0
201
- ANYCABLE_RPC_HOST: anycable:50051
202
- ANYCABLE_DEBUG: 1
203
- depends_on:
204
- redis:
205
- condition: service_healthy
206
-
207
- anycable:
208
- <<: *backend
209
- command: bundle exec anycable
210
- environment:
211
- <<: *backend_environment
212
- ANYCABLE_REDIS_URL: redis://redis:6379/0
213
- ANYCABLE_RPC_HOST: 0.0.0.0:50051
214
- ANYCABLE_DEBUG: 1
215
- ports:
216
- - '50051'
217
- depends_on:
218
- <<: *backend_depends_on
219
- ws:
220
- condition: service_started
221
- ─────────────────────────────────────────
222
- YML
157
+ say "Here is an example snippet for Docker Compose:"
158
+
159
+ if @rpc_impl == "grpc"
160
+ say <<~YML
161
+ ─────────────────────────────────────────
162
+ # your Rails application service
163
+ rails: &rails
164
+ # ...
165
+ ports:
166
+ - '3000:3000'
167
+ environment: &rails_environment
168
+ # ...
169
+ ANYCABLE_HTTP_BROADCAST_URL: http://ws:8090/_broadcast
170
+ depends_on: &rails_depends_on
171
+ #...
172
+ anycable:
173
+ condition: service_started
174
+
175
+ ws:
176
+ image: anycable/anycable-go:1.5
177
+ ports:
178
+ - '8080:8080'
179
+ - '8090'
180
+ environment:
181
+ ANYCABLE_HOST: "0.0.0.0"
182
+ ANYCABLE_BROADCAST_ADAPTER: http
183
+ ANYCABLE_RPC_HOST: anycable:50051
184
+ ANYCABLE_DEBUG: ${ANYCABLE_DEBUG:-true}
185
+
186
+ anycable:
187
+ <<: *rails
188
+ command: bundle exec anycable
189
+ environment:
190
+ <<: *rails_environment
191
+ ANYCABLE_RPC_HOST: 0.0.0.0:50051
192
+ ports:
193
+ - '50051'
194
+ depends_on:
195
+ <<: *rails_depends_on
196
+ ws:
197
+ condition: service_started
198
+ ─────────────────────────────────────────
199
+ YML
200
+ else
201
+ say <<~YML
202
+ ─────────────────────────────────────────
203
+ # Your Rails application service
204
+ rails: &rails
205
+ # ...
206
+ ports:
207
+ - '3000:3000'
208
+ environment: &rails_environment
209
+ # ...
210
+ ANYCABLE_HTTP_BROADCAST_URL: http://ws:8090/_broadcast
211
+ depends_on: &rails_depends_on
212
+ #...
213
+ anycable:
214
+ condition: service_started
215
+
216
+ ws:
217
+ image: anycable/anycable-go:1.5
218
+ ports:
219
+ - '8080:8080'
220
+ environment:
221
+ ANYCABLE_HOST: "0.0.0.0"
222
+ ANYCABLE_BROADCAST_ADAPTER: http
223
+ ANYCABLE_RPC_HOST: http://rails:3000/_anycable
224
+ ANYCABLE_DEBUG: ${ANYCABLE_DEBUG:-true}
225
+ ─────────────────────────────────────────
226
+ YML
227
+ end
223
228
  end
224
229
 
225
230
  def install_for_local
226
- inside("bin") do
227
- template "anycable-go"
228
- chmod "anycable-go", 0755, verbose: false # rubocop:disable Style/NumericLiteralPrefix
231
+ unless file_exists?("bin/anycable-go")
232
+ generate "anycable:bin", "--version #{options[:version]}"
229
233
  end
234
+ template_proc_files
235
+ true
236
+ end
230
237
 
231
- if file_exists?(".gitignore")
232
- append_file ".gitignore", "bin/dist\n"
233
- end
238
+ def update_cable_yml
239
+ if file_exists?("config/cable.yml")
240
+ in_root do
241
+ contents = File.read("config/cable.yml")
242
+ # Replace any adapter: x with any_cable unless x == "test"
243
+ new_contents = contents.gsub(/\sadapter:\s*([^$\n]+)/) do |match|
244
+ adapter = Regexp.last_match[1]
245
+ next match if adapter == "test" || adapter.include?("any_cable")
234
246
 
235
- template_proc_files
247
+ match.sub(adapter, %(<%= ENV.fetch("ACTION_CABLE_ADAPTER", "any_cable") %>))
248
+ end
249
+
250
+ File.write "config/cable.yml", new_contents
251
+ end
252
+ else
253
+ inside("config") do
254
+ template "cable.yml"
255
+ end
256
+ end
236
257
  end
237
258
 
238
259
  def template_proc_files
239
260
  file_name = "Procfile.dev"
240
261
 
241
262
  if file_exists?(file_name)
242
- unless http_rpc?
243
- append_file file_name, "anycable: bundle exec anycable\n", force: true
244
- end
245
- append_file file_name, "ws: bin/anycable-go #{anycable_go_options}", force: true
263
+ update_procfile(file_name)
246
264
  else
247
- say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow
265
+ say_status :help, "💡 We recommend using Overmind to manage multiple processes in development 👉 https://github.com/DarthSim/overmind", :yellow
248
266
 
249
- if options[:skip_procfile_dev].nil?
250
- return unless yes? "Do you want to create a '#{file_name}' file?"
251
- elsif options[:skip_procfile_dev]
252
- return
253
- end
267
+ return if options[:skip_procfile_dev]
254
268
 
255
269
  template file_name
256
270
  end
257
271
  end
258
272
 
273
+ def update_procfile(file_name)
274
+ in_root do
275
+ contents = File.read(file_name)
276
+
277
+ unless http_rpc?
278
+ unless contents.match?(/^anycable:\s/)
279
+ append_file file_name, "anycable: bundle exec anycable\n", force: true
280
+ end
281
+ end
282
+ unless contents.match?(/^ws:\s/)
283
+ append_file file_name, "ws: bin/anycable-go #{anycable_go_options}", force: true
284
+ end
285
+ end
286
+ end
287
+
259
288
  def anycable_go_options
260
289
  opts = ["--port=8080"]
261
290
  opts << "--broadcast_adapter=http" unless redis?
262
- opts << "--rpc_impl=http --rpc_host=http://localhost:3000/_anycable" if http_rpc?
291
+ opts << "--rpc_host=http://localhost:3000/_anycable" if http_rpc?
263
292
  opts.join(" ")
264
293
  end
265
294
 
@@ -268,18 +297,5 @@ module AnyCableRailsGenerators
268
297
  return File.file?(name)
269
298
  end
270
299
  end
271
-
272
- def anycable_go_version
273
- @anycable_go_version ||= normalize_version(options[:version]) || "latest"
274
- end
275
-
276
- def normalize_version(version)
277
- return unless version
278
-
279
- # We need a full version for bin/anycable-go script
280
- segments = Gem::Version.new(version).segments
281
- segments << 0 until segments.size >= 3
282
- segments.join(".")
283
- end
284
300
  end
285
301
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  cd $(dirname $0)/..
4
4
 
5
- # It's recommended to use the exact version of AnyCable-Go here
5
+ # It's recommended to use the exact version of AnyCable here
6
6
  version="<%= anycable_go_version %>"
7
7
 
8
8
  if [ ! -f ./bin/dist/anycable-go ]; then
9
- echo "AnyCable-go is not installed, downloading..."
9
+ echo "AnyCable server is not installed, downloading..."
10
10
  ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist
11
11
  fi
12
12
 
@@ -14,7 +14,7 @@ curVersion=$(./bin/dist/anycable-go -v)
14
14
 
15
15
  if [[ "$version" != "latest" ]]; then
16
16
  if [[ "$curVersion" != "$version"* ]]; then
17
- echo "AnyCable-go version is not $version, downloading a new one..."
17
+ echo "AnyCable server version is not $version, downloading a new one..."
18
18
  ./bin/rails g anycable:download --version=$version --bin-path=./bin/dist
19
19
  fi
20
20
  fi
@@ -21,26 +21,33 @@ default: &default
21
21
  # Use NATS to broadcast messages to AnyCable server
22
22
  broadcast_adapter: nats
23
23
  <%- else -%>
24
- # Use HTTP adapter for a quick start (since redis gem is not present in the project)
24
+ # Use HTTP broadcaster
25
25
  broadcast_adapter: http
26
26
  <%- end -%>
27
- # Use the same channel name for WebSocket server, e.g.:
28
- # $ anycable-go --redis_channel="__anycable__"
29
- redis_channel: "__anycable__"
30
27
  <%- if redis? -%>
31
28
  # You can use REDIS_URL env var to configure Redis URL.
32
29
  # Localhost is used by default.
33
30
  # redis_url: "redis://localhost:6379/1"
31
+ # Use the same channel name for WebSocket server, e.g.:
32
+ # $ anycable-go --redis_channel="__anycable__"
33
+ # redis_channel: "__anycable__"
34
34
  <%- end -%>
35
35
  <%- if http_rpc? -%>
36
+ # Use HTTP RPC mounted at the specified path of your web server
37
+ # Read more about AnyCable RPC: <%= DOCS_ROOT %>/anycable-go/rpc
36
38
  http_rpc_mount_path: "/_anycable"
37
39
  <%- end -%>
38
40
 
39
41
  development:
40
42
  <<: *default
43
+ # WebSocket endpoint of your AnyCable server for clients to connect to
44
+ # Make sure you have the `action_cable_meta_tag` in your HTML layout
45
+ # to propogate this value to the client app
46
+ websocket_url: "ws://localhost:8080/cable"
41
47
 
42
48
  test:
43
49
  <<: *default
44
50
 
45
51
  production:
46
52
  <<: *default
53
+ websocket_url: ~
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-rails-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.4
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-08 00:00:00.000000000 Z
11
+ date: 2024-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.4'
19
+ version: 1.5.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.4'
26
+ version: 1.5.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: actioncable
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +70,7 @@ files:
70
70
  - lib/anycable/rails/action_cable_ext/channel.rb
71
71
  - lib/anycable/rails/action_cable_ext/connection.rb
72
72
  - lib/anycable/rails/action_cable_ext/remote_connections.rb
73
+ - lib/anycable/rails/action_cable_ext/signed_streams.rb
73
74
  - lib/anycable/rails/channel_state.rb
74
75
  - lib/anycable/rails/compatibility.rb
75
76
  - lib/anycable/rails/compatibility/rubocop.rb
@@ -83,12 +84,20 @@ files:
83
84
  - lib/anycable/rails/connections/persistent_session.rb
84
85
  - lib/anycable/rails/connections/serializable_identification.rb
85
86
  - lib/anycable/rails/connections/session_proxy.rb
87
+ - lib/anycable/rails/ext.rb
88
+ - lib/anycable/rails/ext/jwt.rb
89
+ - lib/anycable/rails/helper.rb
86
90
  - lib/anycable/rails/middlewares/executor.rb
87
91
  - lib/anycable/rails/middlewares/log_tagging.rb
92
+ - lib/anycable/rails/object_serializer.rb
93
+ - lib/anycable/rails/pubsub_channel.rb
88
94
  - lib/anycable/rails/rack.rb
89
95
  - lib/anycable/rails/railtie.rb
90
96
  - lib/anycable/rails/socket_id_tracking.rb
91
97
  - lib/anycable/rails/version.rb
98
+ - lib/generators/anycable/bin/USAGE
99
+ - lib/generators/anycable/bin/bin_generator.rb
100
+ - lib/generators/anycable/bin/templates/bin/anycable-go.tt
92
101
  - lib/generators/anycable/download/USAGE
93
102
  - lib/generators/anycable/download/download_generator.rb
94
103
  - lib/generators/anycable/setup/USAGE