anycable-rails 0.6.5 → 1.0.0.preview1

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: d538541df27b5712aadd225d788d43c7d6de563a15dcbb85dd9ef51ff1b13be4
4
- data.tar.gz: e7e5c055860a07f2554af925cee5076a07f97cfcdfa7d540891cb187bbc32f72
3
+ metadata.gz: 38f23428dcd1556e40cdd060257acfd1f1987fbde5b567e8ede65d25b8adde5c
4
+ data.tar.gz: 0b03ddc51e2cd9f9f807fa2aea0f086d302501fbc585e6aa83079f93a4a24864
5
5
  SHA512:
6
- metadata.gz: 007b964f71b796feb189e76d82f2ae3e16c679c56811b9a1b7105bc5663e5e432bcebe4f9a16a971e3819be658f2fcfb8198576d7c9d75495dcc9dcd9db7949e
7
- data.tar.gz: 257f761cfdd1fc3b05d8e1446297a7b31119c667e88e73db1a72485252f5f57e672ee6ac3c39b72e32286b4994d7b62f30f888c4aa12039202c8ccc93fb8df38
6
+ metadata.gz: fb3ede8fe975ca5d7e20cf4f3b51eeb37e1fc0ce94a356bbf21b73c64a212cdc026684b371750280f3bf99176b219f201cf596fb54ebbaaa5eeda4476e8a8fce
7
+ data.tar.gz: e8bb32e3c81fdd83851cc7d0a494b645c0b2407128fc380c11178623f7aef1372971ed391ed72b79bfc0521ce8f70b7e25d97ec75cb8a66a2e614b38947b5aed
@@ -1,13 +1,23 @@
1
1
  # Change log
2
2
 
3
- ## master
3
+ ## 🚧 1.0.0 (_coming soon_)
4
4
 
5
- ## 0.6.5 (2020-02-05)
5
+ - **Ruby 2.5+ is required**. ([@palkan][])
6
6
 
7
- - Make channel not found exception more descriptive. ([@palkan][])
7
+ - Support `disconnect` messages. ([@palkan][])
8
8
 
9
- Previusly, we returned `Undefined method handle_subscribe for nil` when Action Cable
10
- couldn't resolve the channel class. Now we return `Channel not found: <name>`.
9
+ Added in Rails 6 (see [PR#34194](https://github.com/rails/rails/pull/34194)).
10
+
11
+ - Add ability to persist _dirty_ `request.session` between RPC calls. ([@palkan][])
12
+
13
+ This feature emulates the Action Cable behaviour where it's possible to use `request.session` as a shared Hash-like store.
14
+ This could be used by some applications (e.g., [StimulusReflex](https://github.com/hopsoft/stimulus_reflex)-based).
15
+
16
+ You must turn this feature on by setting `persistent_session_enabled: true` in the AnyCable configuration.
17
+
18
+ - Add ability to use Rack middlewares when build a request for a connection. ([@bibendi][])
19
+
20
+ - Add set up generator to configure a Rails application by running `bin/rails g anycable:setup`. ([@bibendi][])
11
21
 
12
22
  - Require a minimum version of Ruby when installing the gem. ([@bibendi][])
13
23
 
data/README.md CHANGED
@@ -20,7 +20,7 @@ You can even use Action Cable in development and not be afraid of [compatibility
20
20
 
21
21
  ## Requirements
22
22
 
23
- - Ruby >= 2.4; **NOTE:** for Ruby 2.6 use RC version of `google-protobuf` gem. In your Gemfile: `gem "google-protobuf", '>=3.7.0.rc.2'`
23
+ - Ruby >= 2.5
24
24
  - Rails >= 5.0;
25
25
  - Redis (see [other options]() for broadcasting)
26
26
 
@@ -30,7 +30,6 @@ You can even use Action Cable in development and not be afraid of [compatibility
30
30
 
31
31
  ## Usage
32
32
 
33
-
34
33
  Add `anycable-rails` gem to your Gemfile:
35
34
 
36
35
  ```ruby
@@ -42,12 +41,25 @@ gem "redis", ">= 4.0"
42
41
 
43
42
  (and don't forget to run `bundle install`).
44
43
 
45
- Next, specify AnyCable subscription adapter for Action Cable:
44
+ ### Interactive set up
45
+
46
+ After the gem was installed, you can run an interactive wizard to configure your Rails application for using with AnyCable by running a generator:
47
+
48
+ ```sh
49
+ bin/rails g anycable:setup
50
+ ```
51
+
52
+ ### Manual set up
53
+
54
+ Specify AnyCable subscription adapter for Action Cable:
46
55
 
47
56
  ```yml
48
57
  # config/cable.yml
49
- production:
58
+ development:
50
59
  adapter: any_cable # or anycable
60
+
61
+ production:
62
+ adapter: any_cable
51
63
  ```
52
64
 
53
65
  and specify AnyCable WebSocket server URL:
@@ -66,7 +78,7 @@ config.action_cable.url = "wss://ws.example.com/cable"
66
78
 
67
79
  Then, run AnyCable RPC server:
68
80
 
69
- ```ruby
81
+ ```sh
70
82
  $ bundle exec anycable
71
83
 
72
84
  # don't forget to provide Rails env
@@ -7,7 +7,8 @@ module ActionCable
7
7
  # AnyCable subscription adapter delegates broadcasts
8
8
  # to AnyCable
9
9
  class AnyCable < Base
10
- def initialize(*); end
10
+ def initialize(*)
11
+ end
11
12
 
12
13
  def broadcast(channel, payload)
13
14
  ::AnyCable.broadcast(channel, payload)
@@ -3,6 +3,7 @@
3
3
  require "anycable"
4
4
  require "anycable/rails/version"
5
5
  require "anycable/rails/config"
6
+ require "anycable/rails/rack"
6
7
 
7
8
  module AnyCable
8
9
  # Rails handler for AnyCable
@@ -3,12 +3,13 @@
3
3
  require "action_cable/connection"
4
4
  require "anycable/rails/refinements/subscriptions"
5
5
  require "anycable/rails/actioncable/channel"
6
+ require "anycable/rails/session_proxy"
6
7
 
7
8
  module ActionCable
8
9
  module Connection
9
10
  # rubocop: disable Metrics/ClassLength
10
11
  class Base # :nodoc:
11
- # We store logger tags in identifiers to be able
12
+ # We store logger tags in the connection state to be able
12
13
  # to re-use them in the subsequent calls
13
14
  LOG_TAGS_IDENTIFIER = "__ltags__"
14
15
 
@@ -16,6 +17,8 @@ module ActionCable
16
17
 
17
18
  attr_reader :socket
18
19
 
20
+ delegate :env, :session, to: :request
21
+
19
22
  class << self
20
23
  def call(socket, **options)
21
24
  new(socket, options)
@@ -34,10 +37,12 @@ module ActionCable
34
37
  def initialize(socket, identifiers: "{}", subscriptions: [])
35
38
  @ids = ActiveSupport::JSON.decode(identifiers)
36
39
 
37
- @ltags = ids.delete(LOG_TAGS_IDENTIFIER)
40
+ @ltags = socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
41
+ next unless raw_tags
42
+ ActiveSupport::JSON.decode(raw_tags)
43
+ end
38
44
 
39
45
  @cached_ids = {}
40
- @env = socket.env
41
46
  @coder = ActiveSupport::JSON
42
47
  @socket = socket
43
48
  @subscriptions = ActionCable::Connection::Subscriptions.new(self)
@@ -49,12 +54,17 @@ module ActionCable
49
54
  def handle_open
50
55
  logger.info started_request_message if access_logs?
51
56
 
52
- verify_origin!
57
+ verify_origin! || return
53
58
 
54
59
  connect if respond_to?(:connect)
60
+
61
+ socket.cstate.write(LOG_TAGS_IDENTIFIER, fetch_ltags.to_json)
62
+
55
63
  send_welcome_message
56
64
  rescue ActionCable::Connection::Authorization::UnauthorizedError
57
- reject_request
65
+ reject_request(
66
+ ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
67
+ )
58
68
  end
59
69
 
60
70
  def handle_close
@@ -84,7 +94,12 @@ module ActionCable
84
94
  end
85
95
  # rubocop:enable Metrics/MethodLength
86
96
 
87
- def close
97
+ def close(reason: nil, reconnect: nil)
98
+ transmit(
99
+ type: ActionCable::INTERNAL[:message_types].fetch(:disconnect, "disconnect"),
100
+ reason: reason,
101
+ reconnect: reconnect
102
+ )
88
103
  socket.close
89
104
  end
90
105
 
@@ -95,9 +110,7 @@ module ActionCable
95
110
  # Generate identifiers info.
96
111
  # Converts GlobalID compatible vars to corresponding global IDs params.
97
112
  def identifiers_hash
98
- obj = { LOG_TAGS_IDENTIFIER => fetch_ltags }
99
-
100
- identifiers.each_with_object(obj) do |id, acc|
113
+ identifiers.each_with_object({}) do |id, acc|
101
114
  obj = instance_variable_get("@#{id}")
102
115
  next unless obj
103
116
 
@@ -123,6 +136,10 @@ module ActionCable
123
136
  @logger ||= TaggedLoggerProxy.new(AnyCable.logger, tags: ltags || [])
124
137
  end
125
138
 
139
+ def request
140
+ @request ||= build_rack_request
141
+ end
142
+
126
143
  private
127
144
 
128
145
  attr_reader :ids, :ltags
@@ -158,19 +175,33 @@ module ActionCable
158
175
  end
159
176
 
160
177
  def verify_origin!
161
- return unless socket.env.key?("HTTP_ORIGIN")
178
+ return true unless socket.env.key?("HTTP_ORIGIN")
162
179
 
163
- return if allow_request_origin?
180
+ return true if allow_request_origin?
164
181
 
165
- raise(
166
- ActionCable::Connection::Authorization::UnauthorizedError,
167
- "Origin is not allowed"
182
+ reject_request(
183
+ ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
168
184
  )
185
+ false
169
186
  end
170
187
 
171
- def reject_request
188
+ def reject_request(reason, reconnect = false)
172
189
  logger.info finished_request_message("Rejected") if access_logs?
173
- close
190
+ close(
191
+ reason: reason,
192
+ reconnect: reconnect
193
+ )
194
+ end
195
+
196
+ def build_rack_request
197
+ environment = Rails.application.env_config.merge(socket.env)
198
+ AnyCable::Rails::Rack.app.call(environment)
199
+
200
+ ActionDispatch::Request.new(environment)
201
+ end
202
+
203
+ def request_loaded?
204
+ instance_variable_defined?(:@request)
174
205
  end
175
206
  end
176
207
  # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Connection
5
+ module PersistentSession
6
+ def handle_open
7
+ super.tap { commit_session! }
8
+ end
9
+
10
+ def handle_channel_command(*)
11
+ super.tap { commit_session! }
12
+ end
13
+
14
+ def build_rack_request
15
+ return super unless socket.session
16
+
17
+ super.tap do |req|
18
+ req.env[Rack::RACK_SESSION] =
19
+ AnyCable::Rails::SessionProxy.new(req.env[Rack::RACK_SESSION], socket.session)
20
+ end
21
+ end
22
+
23
+ def commit_session!
24
+ return unless request_loaded? && request.session.loaded?
25
+
26
+ socket.session = request.session.to_json
27
+ end
28
+ end
29
+ end
30
+ end
@@ -12,7 +12,7 @@ module AnyCable
12
12
 
13
13
  if callback.present? || block_given?
14
14
  raise AnyCable::CompatibilityError,
15
- "Custom stream callbacks are not supported by AnyCable"
15
+ "Custom stream callbacks are not supported by AnyCable"
16
16
  end
17
17
 
18
18
  super
@@ -35,8 +35,8 @@ module AnyCable
35
35
 
36
36
  unless diff.empty?
37
37
  raise AnyCable::CompatibilityError,
38
- "Channel instance variables are not supported by AnyCable, " \
39
- "but were set: #{diff.join(', ')}"
38
+ "Channel instance variables are not supported by AnyCable, " \
39
+ "but were set: #{diff.join(", ")}"
40
40
  end
41
41
 
42
42
  res
@@ -52,7 +52,7 @@ module AnyCable
52
52
  ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
53
53
  def disconnect
54
54
  raise AnyCable::CompatibilityError,
55
- "Disconnecting remote clients is not supported by AnyCable yet"
55
+ "Disconnecting remote clients is not supported by AnyCable yet"
56
56
  end
57
57
  end)
58
58
  end
@@ -9,4 +9,3 @@ AnyCable/StreamFrom:
9
9
  AnyCable/PeriodicalTimers:
10
10
  Include:
11
11
  - "**/channels/**/*.rb"
12
-
@@ -2,7 +2,11 @@
2
2
 
3
3
  require "anycable/config"
4
4
 
5
- # Extend AnyCable configuration with
6
- # `access_logs_disabled` options (defaults to true)
7
- AnyCable::Config.attr_config access_logs_disabled: true
8
- AnyCable::Config.ignore_options :access_logs_disabled
5
+ # Extend AnyCable configuration with:
6
+ # - `access_logs_disabled` (defaults to true) — whether to print Started/Finished logs
7
+ # - `persistent_session_enabled` (defaults to false) — whether to store session changes in the connection state
8
+ AnyCable::Config.attr_config(
9
+ access_logs_disabled: true,
10
+ persistent_session_enabled: false
11
+ )
12
+ AnyCable::Config.ignore_options :access_logs_disabled, :persistent_session_enabled
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/configuration"
4
+ require "action_dispatch/middleware/stack"
5
+
6
+ module AnyCable
7
+ module Rails
8
+ # Rack middleware stack to modify the HTTP request object.
9
+ #
10
+ # AnyCable Websocket server does not use Rack middleware processing mechanism (which Rails uses
11
+ # when Action Cable is mounted into the main app).
12
+ #
13
+ # Some middlewares could enhance request env with useful information.
14
+ #
15
+ # For instance, consider the Rails session middleware: it's responsible for restoring the
16
+ # session data from cookies.
17
+ #
18
+ # AnyCable adds session middelware by default to its own stack.
19
+ #
20
+ # You can also use any Rack/Rails middleware you want. For example, to enable Devise/Warden
21
+ # you can add the following code to an initializer or any other configuration file:
22
+ #
23
+ # AnyCable::Rails::Rack.middleware.use Warden::Manager do |config|
24
+ # Devise.warden_config = config
25
+ # end
26
+ module Rack
27
+ def self.app_build_lock
28
+ @app_build_lock
29
+ end
30
+
31
+ @app_build_lock = Mutex.new
32
+
33
+ def self.middleware
34
+ @middleware ||= ::Rails::Configuration::MiddlewareStackProxy.new
35
+ end
36
+
37
+ def self.default_middleware_stack
38
+ config = ::Rails.application.config
39
+
40
+ ActionDispatch::MiddlewareStack.new do |middleware|
41
+ middleware.use(config.session_store, config.session_options)
42
+ end
43
+ end
44
+
45
+ def self.app
46
+ @rack_app || app_build_lock.synchronize do
47
+ @rack_app ||= begin
48
+ stack = default_middleware_stack
49
+ @middleware = middleware.merge_into(stack)
50
+ middleware.build { [-1, {}, []] }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -16,7 +16,7 @@ module AnyCable
16
16
 
17
17
  # Broadcast server logs to STDOUT in development
18
18
  if ::Rails.env.development? &&
19
- !ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, STDOUT)
19
+ !ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, STDOUT)
20
20
  AnyCable.configure_server do
21
21
  console = ActiveSupport::Logger.new(STDOUT)
22
22
  console.formatter = ::Rails.logger.formatter
@@ -46,6 +46,13 @@ module AnyCable
46
46
  if AnyCable::Rails.compatible_adapter?(adapter)
47
47
  require "anycable/rails/actioncable/connection"
48
48
 
49
+ if AnyCable.config.persistent_session_enabled
50
+ require "anycable/rails/actioncable/connection/persistent_session"
51
+ ::ActionCable::Connection::Base.prepend(
52
+ ::ActionCable::Connection::PersistentSession
53
+ )
54
+ end
55
+
49
56
  app.config.to_prepare do
50
57
  AnyCable.connection_factory = ActionCable.server.config.connection_class.call
51
58
  end
@@ -7,11 +7,6 @@ module AnyCable
7
7
  # Find or add a subscription to the list
8
8
  def fetch(identifier)
9
9
  add("identifier" => identifier) unless subscriptions[identifier]
10
-
11
- unless subscriptions[identifier]
12
- raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch('channel')}"
13
- end
14
-
15
10
  subscriptions[identifier]
16
11
  end
17
12
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ # Wrap `request.session` to lazily load values provided
6
+ # in the RPC call (set by the previous calls)
7
+ class SessionProxy
8
+ delegate_missing_to :@rack_session
9
+
10
+ attr_reader :rack_session, :socket_session
11
+
12
+ def initialize(rack_session, socket_session)
13
+ @rack_session = rack_session
14
+ @socket_session = JSON.parse(socket_session).with_indifferent_access
15
+ end
16
+
17
+ %i[has_key? [] []= fetch delete dig].each do |mid|
18
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
19
+ def #{mid}(*args, **kwargs, &block)
20
+ restore_key! args.first
21
+ rack_session.#{mid}(*args, **kwargs, &block)
22
+ end
23
+ CODE
24
+ end
25
+
26
+ alias include? has_key?
27
+ alias key? has_key?
28
+
29
+ %i[update merge! to_hash].each do |mid|
30
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
31
+ def #{mid}(*args, **kwargs, &block)
32
+ restore!
33
+ rack_session.#{mid}(*args, **kwargs, &block)
34
+ end
35
+ CODE
36
+ end
37
+
38
+ alias to_h to_hash
39
+
40
+ def keys
41
+ rack_session.keys + socket_session.keys
42
+ end
43
+
44
+ private
45
+
46
+ def restore!
47
+ socket_session.keys.each(&method(:restore_key!))
48
+ end
49
+
50
+ def restore_key!(key)
51
+ return unless socket_session.key?(key)
52
+ val = socket_session.delete(key)
53
+ rack_session[key] =
54
+ if val.is_a?(String)
55
+ GlobalID::Locator.locate(val) || val
56
+ else
57
+ val
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "0.6.5"
5
+ VERSION = "1.0.0.preview1"
6
6
  end
7
7
  end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCableRailsGenerators
4
+ # Entry point for interactive installation
5
+ class SetupGenerator < ::Rails::Generators::Base
6
+ namespace "anycable:setup"
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ METHODS = %w[skip local docker].freeze
10
+ SERVER_VERSION = "v0.6.4"
11
+ OS_NAMES = %w[linux darwin freebsd win].freeze
12
+ CPU_NAMES = %w[amd64 arm64 386 arm].freeze
13
+ SERVER_SOURCES = %w[skip brew binary].freeze
14
+ DEFAULT_BIN_PATH = "/usr/local/bin"
15
+
16
+ class_option :method,
17
+ type: :string,
18
+ desc: "Select your development environment (options: #{METHODS.join(", ")})"
19
+ class_option :source,
20
+ type: :string,
21
+ desc: "Choose a way of installing AnyCable-Go server (options: #{SERVER_SOURCES.join(", ")})"
22
+ class_option :bin_path,
23
+ type: :string,
24
+ desc: "Where to download AnyCable-Go server binary (default: #{DEFAULT_BIN_PATH})"
25
+ class_option :os,
26
+ type: :string,
27
+ desc: "Specify the OS for AnyCable-Go server binary (options: #{OS_NAMES.join(", ")})"
28
+ class_option :cpu,
29
+ type: :string,
30
+ desc: "Specify the CPU architecturefor AnyCable-Go server binary (options: #{CPU_NAMES.join(", ")})"
31
+ class_option :skip_heroku,
32
+ type: :boolean,
33
+ desc: "Do not copy Heroku configs"
34
+ class_option :skip_procfile_dev,
35
+ type: :boolean,
36
+ desc: "Do not create Procfile.dev"
37
+
38
+ def welcome
39
+ say "👋 Welcome to AnyCable interactive installer."
40
+ end
41
+
42
+ def configs
43
+ inside("config") do
44
+ template "cable.yml"
45
+ template "anycable.yml"
46
+ end
47
+ end
48
+
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.action_cable.url = ENV.fetch("CABLE_URL", "ws://localhost:3334/cable").presence
54
+ SNIPPET
55
+ end
56
+
57
+ environment(nil, env: :production) do
58
+ <<~SNIPPET
59
+ # Specify AnyCable WebSocket server URL to use by JS client
60
+ config.action_cable.url = ENV["CABLE_URL"].presence
61
+ SNIPPET
62
+ end
63
+
64
+ say_status :info, "✅ 'config.action_cable.url' has been configured"
65
+ say_status :help, "⚠️ If you're using JS client make sure you have " \
66
+ "`action_cable_meta_tag` included before any <script> tag in your application.html"
67
+ end
68
+
69
+ def development_method
70
+ answer = METHODS.index(options[:method]) || 99
71
+
72
+ until METHODS[answer.to_i]
73
+ answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (0) Skip"
74
+ end
75
+
76
+ case env = METHODS[answer.to_i]
77
+ when "skip"
78
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow
79
+ else
80
+ send "install_for_#{env}"
81
+ end
82
+ end
83
+
84
+ def heroku
85
+ if options[:skip_heroku].nil?
86
+ return unless yes? "Do you use Heroku for deployment?"
87
+ elsif options[:skip_heroku]
88
+ return
89
+ end
90
+
91
+ template "Procfile"
92
+ inside("bin") { template "heroku-web" }
93
+
94
+ say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 https://docs.anycable.io/#/deployment/heroku", :yellow
95
+ end
96
+
97
+ def devise
98
+ in_root do
99
+ return unless File.file?("config/initializers/devise.rb")
100
+ end
101
+
102
+ inside("config/initializers") do
103
+ template "anycable.rb"
104
+ end
105
+
106
+ say_status :info, "✅ config/initializers/anycable.rb with Devise configuration has been added"
107
+ end
108
+
109
+ def finish
110
+ say_status :info, "✅ AnyCable has been configured successfully!"
111
+ end
112
+
113
+ private
114
+
115
+ def stimulus_reflex?
116
+ !gemfile_lock&.match?(/^\s+stimulus_reflex\b/).nil?
117
+ end
118
+
119
+ def gemfile_lock
120
+ @gemfile_lock ||= begin
121
+ res = nil
122
+ in_root do
123
+ next unless File.file?("Gemfile.lock")
124
+ res = File.read("Gemfile.lock")
125
+ end
126
+ res
127
+ end
128
+ end
129
+
130
+ def install_for_docker
131
+ say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
132
+
133
+ say "Here is an example snippet for docker-compose.yml:"
134
+ say <<~YML
135
+ ─────────────────────────────────────────
136
+ anycable-ws:
137
+ image: anycable/anycable-go:v0.6.4
138
+ ports:
139
+ - '3334:3334'
140
+ environment:
141
+ PORT: 3334
142
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
143
+ ANYCABLE_RPC_HOST: anycable-rpc:50051
144
+ depends_on:
145
+ - anycable-rpc
146
+ - redis
147
+
148
+ anycable-rpc:
149
+ <<: *backend
150
+ command: bundle exec anycable
151
+ environment:
152
+ <<: *backend_environment
153
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
154
+ ANYCABLE_RPC_HOST: 0.0.0.0:50051
155
+ ports:
156
+ - '50051'
157
+ ─────────────────────────────────────────
158
+ YML
159
+ end
160
+
161
+ def install_for_local
162
+ install_server
163
+ template_proc_files
164
+ end
165
+
166
+ def install_server
167
+ answer = SERVER_SOURCES.index(options[:source]) || 99
168
+
169
+ until SERVER_SOURCES[answer.to_i]
170
+ answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (0) Skip"
171
+ end
172
+
173
+ case answer.to_i
174
+ when 0
175
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow
176
+ return
177
+ else
178
+ return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}")
179
+ end
180
+
181
+ say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed"
182
+ end
183
+
184
+ def template_proc_files
185
+ file_name = "Procfile.dev"
186
+
187
+ if File.exist?(file_name)
188
+ append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"'
189
+ else
190
+ say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow
191
+
192
+ if options[:skip_procfile_dev].nil?
193
+ return unless yes? "Do you want to create a '#{file_name}' file?"
194
+ elsif options[:skip_procfile_dev]
195
+ return
196
+ end
197
+
198
+ template file_name
199
+ end
200
+ end
201
+
202
+ def install_from_brew
203
+ run "brew install anycable-go", abort_on_failure: true
204
+ run "anycable-go -v", abort_on_failure: true
205
+ end
206
+
207
+ def install_from_binary
208
+ out = options[:bin_path] if options[:bin_path]
209
+ out ||= "/usr/local/bin" if options[:method] # User don't want interactive mode
210
+ out ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true
211
+
212
+ os_name = options[:os] ||
213
+ OS_NAMES.find(&Gem::Platform.local.os.method(:==)) ||
214
+ ask("What is your OS name?", limited_to: OS_NAMES)
215
+
216
+ cpu_name = options[:cpu] ||
217
+ CPU_NAMES.find(&current_cpu.method(:==)) ||
218
+ ask("What is your CPU architecture?", limited_to: CPU_NAMES)
219
+
220
+ download_exe(
221
+ "https://github.com/anycable/anycable-go/releases/download/#{SERVER_VERSION}/" \
222
+ "anycable-go-#{SERVER_VERSION}-#{os_name}-#{cpu_name}",
223
+ to: out,
224
+ file_name: "anycable-go"
225
+ )
226
+
227
+ true
228
+ end
229
+
230
+ def download_exe(url, to:, file_name:)
231
+ file_path = File.join(to, file_name)
232
+
233
+ run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true
234
+ run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true
235
+ run "#{file_path} -v", abort_on_failure: true
236
+ end
237
+
238
+ def sudo!(path)
239
+ sudo = ""
240
+ unless File.writable?(path)
241
+ if yes? "Path is not writable 😕. Do you have sudo privileges?"
242
+ sudo = "sudo "
243
+ else
244
+ say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
245
+ raise StandardError, "Path #{path} is not writable!"
246
+ end
247
+ end
248
+
249
+ sudo
250
+ end
251
+
252
+ def current_cpu
253
+ case Gem::Platform.local.cpu
254
+ when "x86_64", "x64"
255
+ "amd64"
256
+ when "x86_32", "x86", "i386", "i486", "i686"
257
+ "i386"
258
+ when "aarch64", "aarch64_be", /armv8/
259
+ "arm64"
260
+ when "arm", /armv7/, /armv6/
261
+ "arm"
262
+ else
263
+ "unknown"
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,2 @@
1
+ web: bin/heroku-web
2
+ release: bundle exec rails db:migrate
@@ -0,0 +1,3 @@
1
+ server: bin/rails server
2
+ assets: bin/webpack-dev-server
3
+ anycable: bundle exec anycable --server-command "anycable-go --port 3334"
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+
3
+ if [ "$ANYCABLE_DEPLOYMENT" == "true" ]; then
4
+ bundle exec anycable --server-command="anycable-go"
5
+ else
6
+ bundle exec rails server -p $PORT -b 0.0.0.0
7
+ fi
@@ -0,0 +1,32 @@
1
+ # This file contains per-environment settings for AnyCable.
2
+ #
3
+ # Since AnyCable config is based on anyway_config (https://github.com/palkan/anyway_config), all AnyCable settings
4
+ # can be set or overridden through the corresponding environment variables.
5
+ # E.g., `rpc_host` is overridden by ANYCABLE_RPC_HOST, `debug` by ANYCABLE_DEBUG etc.
6
+ #
7
+ # Note that AnyCable recognizes REDIS_URL env variable for Redis pub/sub adapter. If you want to
8
+ # use another Redis instance for AnyCable, provide ANYCABLE_REDIS_URL variable.
9
+ #
10
+ # Read more about AnyCable configuration here: https://docs.anycable.io/#/ruby/configuration
11
+ #
12
+ default: &default
13
+ # Turn on/off access logs ("Started..." and "Finished...")
14
+ access_logs_disabled: false
15
+ # Persist "dirty" session between RPC calls (might be required for StimulusReflex apps)
16
+ persistent_session_enabled: <%= stimulus_reflex? %>
17
+ # This is the host and the port to run AnyCable RPC server on.
18
+ # You must configure your WebSocket server to connect to it, e.g.:
19
+ # $ anycable-go --rpc-host="<rpc hostname>:50051"
20
+ rpc_host: "127.0.0.1:50051"
21
+ # Whether to enable gRPC level logging or not
22
+ log_grpc: false
23
+ # Use the same channel name for WebSocket server, e.g.:
24
+ # $ anycable-go --redis-channel="__anycable__"
25
+ redis_channel: "__anycable__"
26
+
27
+ development:
28
+ <<: *default
29
+ redis_url: "redis://localhost:6379/1"
30
+
31
+ production:
32
+ <<: *default
@@ -0,0 +1,11 @@
1
+ # Make it possible to switch adapters by passing the CABLE_ADAPTER env variable.
2
+ # For example, you can use it fallback to the standard Action Cable in staging/review
3
+ # environments (by setting `CABLE_ADAPTER=redis`).
4
+ development:
5
+ adapter: <%%= ENV.fetch("CABLE_ADAPTER", "any_cable") %>
6
+
7
+ test:
8
+ adapter: test
9
+
10
+ production:
11
+ adapter: <%%= ENV.fetch("CABLE_ADAPTER", "any_cable") %>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add Warden middkeware to AnyCable stack to allow accessing
4
+ # Devise current user via `env["warden"].user`.
5
+ #
6
+ # See http://docs.anycable.io/#/ruby/authentication
7
+ AnyCable::Rails::Rack.middleware.use Warden::Manager do |config|
8
+ Devise.warden_config = config
9
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 1.0.0.preview1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-05 00:00:00.000000000 Z
11
+ date: 2020-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.6.0
19
+ version: 1.0.0.preview1
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: 0.6.0
26
+ version: 1.0.0.preview1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rails
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,33 +39,33 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '5'
41
41
  - !ruby/object:Gem::Dependency
42
- name: bundler
42
+ name: ammeter
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.10'
47
+ version: '1.1'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.10'
54
+ version: '1.1'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry-byebug
56
+ name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '1.10'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '1.10'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -95,19 +95,19 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.4'
97
97
  - !ruby/object:Gem::Dependency
98
- name: rubocop
98
+ name: rubocop-md
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.67.0
103
+ version: '0.3'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.67.0
110
+ version: '0.3'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: simplecov
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +122,34 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: 0.3.8
125
+ - !ruby/object:Gem::Dependency
126
+ name: standard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.1.7
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.1.7
139
+ - !ruby/object:Gem::Dependency
140
+ name: warden
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  description: Rails adapter for AnyCable
126
154
  email:
127
155
  - dementiev.vm@gmail.com
@@ -138,6 +166,7 @@ files:
138
166
  - lib/anycable/rails.rb
139
167
  - lib/anycable/rails/actioncable/channel.rb
140
168
  - lib/anycable/rails/actioncable/connection.rb
169
+ - lib/anycable/rails/actioncable/connection/persistent_session.rb
141
170
  - lib/anycable/rails/compatibility.rb
142
171
  - lib/anycable/rails/compatibility/rubocop.rb
143
172
  - lib/anycable/rails/compatibility/rubocop/config/default.yml
@@ -148,9 +177,18 @@ files:
148
177
  - lib/anycable/rails/config.rb
149
178
  - lib/anycable/rails/middlewares/executor.rb
150
179
  - lib/anycable/rails/middlewares/log_tagging.rb
180
+ - lib/anycable/rails/rack.rb
151
181
  - lib/anycable/rails/railtie.rb
152
182
  - lib/anycable/rails/refinements/subscriptions.rb
183
+ - lib/anycable/rails/session_proxy.rb
153
184
  - lib/anycable/rails/version.rb
185
+ - lib/rails/generators/anycable/setup/setup_generator.rb
186
+ - lib/rails/generators/anycable/setup/templates/Procfile
187
+ - lib/rails/generators/anycable/setup/templates/Procfile.dev
188
+ - lib/rails/generators/anycable/setup/templates/bin/heroku-web
189
+ - lib/rails/generators/anycable/setup/templates/config/anycable.yml.tt
190
+ - lib/rails/generators/anycable/setup/templates/config/cable.yml.tt
191
+ - lib/rails/generators/anycable/setup/templates/config/initializers/anycable.rb
154
192
  homepage: http://github.com/anycable/anycable-rails
155
193
  licenses:
156
194
  - MIT
@@ -168,12 +206,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
206
  requirements:
169
207
  - - ">="
170
208
  - !ruby/object:Gem::Version
171
- version: '2.4'
209
+ version: '2.5'
172
210
  required_rubygems_version: !ruby/object:Gem::Requirement
173
211
  requirements:
174
- - - ">="
212
+ - - ">"
175
213
  - !ruby/object:Gem::Version
176
- version: '0'
214
+ version: 1.3.1
177
215
  requirements: []
178
216
  rubygems_version: 3.0.6
179
217
  signing_key: