Skip to content

Commit

Permalink
feat: finalize Rails 8 integration
Browse files Browse the repository at this point in the history
  • Loading branch information
palkan committed Sep 13, 2024
1 parent b901112 commit 20c991c
Show file tree
Hide file tree
Showing 19 changed files with 55 additions and 127 deletions.
16 changes: 5 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,21 @@ jobs:
COVERALLS_REPO_TOKEN: ${{ secrets.github_token }}
services:
redis:
image: redis:6.0-alpine
image: redis:7.0-alpine
ports: ["6379:6379"]
options: --health-cmd="redis-cli ping" --health-interval 1s --health-timeout 3s --health-retries 30
strategy:
fail-fast: false
matrix:
ruby: ["3.0"]
gemfile: ["rails6"]
ruby: ["3.2"]
gemfile: ["rails8"]
include:
- ruby: "3.2"
gemfile: "rails7"
- ruby: "2.7"
gemfile: "rails6"
- ruby: "3.1"
- ruby: "3.3"
gemfile: "railsmaster"
- ruby: "2.7"
gemfile: "rails60"
- ruby: "3.2"
gemfile: "anycablemaster"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install system deps
run: |
sudo apt-get update
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ else
gem 'activejob'
end

gem 'sqlite3', '~> 1.3'
gem 'sqlite3', '~> 2.0'
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ AnyCable allows you to use any WebSocket server (written in any language) as a r

With AnyCable you can use channels, client-side JS, broadcasting - (almost) all that you can do with Action Cable.

You can even use Action Cable in development and not be afraid of [compatibility issues](#compatibility).

💾 [Example Application](https://github.com/anycable/anycable_rails_demo)

📑 [Documentation](https://docs.anycable.io/rails/getting_started).
Expand All @@ -21,19 +19,17 @@ You can even use Action Cable in development and not be afraid of [compatibility

## Requirements

- Ruby >= 2.6
- Rails >= 6.0 (Rails 5.1 could work but we're no longer enforce compatibility on CI)
- Redis (see [other options](https://github.com/anycable/anycable/issues/2) for broadcasting)
- Ruby >= 3.1
- Rails >= 6.0\*

\* Recent `anycable-rails` versions only work with Rails 8+; older versions compatible with Rails 6 and Rails 7 still recieve fixes and minor updates (patch releases).

## Usage

Add `anycable-rails` gem to your Gemfile:

```ruby
gem "anycable-rails"

# when using Redis broadcast adapter
gem "redis", ">= 4.0"
```

### Interactive set up
Expand Down
2 changes: 1 addition & 1 deletion anycable-rails-core.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.7"

spec.add_dependency "anycable-core", "~> 1.5.0"
spec.add_dependency "actioncable", ">= 6.0"
spec.add_dependency "actioncable", "> 7.2"
spec.add_dependency "globalid"
end
4 changes: 2 additions & 2 deletions gemfiles/anycablemaster.gemfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
source "https://rubygems.org"

gem "rails", ">= 7.1"
gem "rails", ">= 8.0"
gem "rspec-rails"
gem "anycable", git: "https://github.com/anycable/anycable.git", branch: "master"
gem "sqlite3", "~> 1.4"
gem "sqlite3"

gemspec path: "..", name: "anycable-rails"
9 changes: 0 additions & 9 deletions gemfiles/rails6.gemfile

This file was deleted.

9 changes: 0 additions & 9 deletions gemfiles/rails60.gemfile

This file was deleted.

9 changes: 0 additions & 9 deletions gemfiles/rails7.gemfile

This file was deleted.

6 changes: 3 additions & 3 deletions gemfiles/rails51.gemfile → gemfiles/rails8.gemfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
source "https://rubygems.org"

gem "actioncable", "~> 5.1"
gem "actioncable", "~> 8.0"
gem "activerecord"
gem "activejob"
gem "rspec-rails", "~> 4.0.0"
gem "sqlite3", "~> 1.3"
gem "rspec-rails"
gem "sqlite3"

gemspec path: "..", name: "anycable-rails"
4 changes: 2 additions & 2 deletions gemfiles/railsmaster.gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
source "https://rubygems.org"

gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
gem "rails", git: "https://github.com/palkan/rails.git", branch: "refactor/action-cable-server-adapterization"
gem "rspec-rails"
gem "sqlite3", "~> 1.4"
gem "sqlite3"

gemspec path: "..", name: "anycable-rails"
5 changes: 5 additions & 0 deletions lib/anycable/rails/action_cable_ext/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ def stop_all_streams
connection.anycable_socket.unsubscribe_from_all identifier
end

# Make rejected status accessible from outside
def rejected?
subscription_rejected?
end

private

def anycabled?
Expand Down
14 changes: 2 additions & 12 deletions lib/anycable/rails/action_cable_ext/signed_streams.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,10 @@ def allow_public_streams?

# Handle $pubsub channel in Subscriptions
ActionCable::Connection::Subscriptions.prepend(Module.new do
# The contents are mostly copied from the original,
# there is no good way to configure channels mapping due to #safe_constantize
# and the layers of JSON
# https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/connection/subscriptions.rb
def add(data)
id_key = data["identifier"]
def subscription_from_identifier(id_key)
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access

return if subscriptions.key?(id_key)

return super unless id_options[:channel] == "$pubsub"

subscription = AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
subscriptions[id_key] = subscription
subscription.subscribe_to_channel
AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
end
end)
20 changes: 11 additions & 9 deletions lib/anycable/rails/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,32 @@ def execute_command(data)
cmd = data["command"]
return false unless COMMANDS.include?(cmd)

fetch(data["identifier"]) unless cmd == "subscribe"
load(data["identifier"]) unless cmd == "subscribe"

super

return true unless cmd == "subscribe"

!fetch(data["identifier"])&.rejected?
subscription = subscriptions[data["identifier"]]
!(subscription.nil? || subscription.rejected?)
end

# Restore channels from the list of identifiers and the state
def restore(subscriptions, istate)
subscriptions.each do |id|
channel = fetch(id)
channel = load(id)
channel.__istate__ = ActiveSupport::JSON.decode(istate[id]) if istate[id]
end
end

# Find or create a channel for a given identifier
def fetch(identifier)
add("identifier" => identifier) unless subscriptions[identifier]
def load(identifier)
return subscriptions[identifier] if subscriptions[identifier]

unless subscriptions[identifier]
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
end
subscription = subscription_from_identifier(identifier)
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}" unless subscription

subscriptions[identifier]
subscriptions[identifier] = subscription
end
end

Expand Down Expand Up @@ -84,6 +84,7 @@ def handle_open

def handle_close
conn.handle_close
close
true
end

Expand Down Expand Up @@ -112,6 +113,7 @@ def transmit(data)
end

def close(...)
return if socket.closed?
logger.info finished_request_message if access_logs?
socket.close(...)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/anycable/rails/ext/jwt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def handle_open
super
rescue AnyCable::JWT::ExpiredSignature
logger.error "An expired JWT token was rejected"
close(reason: "token_expired", reconnect: false) if websocket&.alive?
close(reason: "token_expired", reconnect: false)
end

def anycable_jwt_present?
Expand Down
5 changes: 2 additions & 3 deletions spec/dummy/app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ def log_event(source, data)
end
end

if respond_to?(:rescue_from)
rescue_from ActiveRecord::RecordNotFound, with: :track_error
end
rescue_from ActiveRecord::RecordNotFound, with: :track_error

delegate :session, to: :request

Expand Down Expand Up @@ -55,6 +53,7 @@ def verify_user

def track_error(e)
self.class.log_event("error", message: e.message)
raise e
end
end
end
7 changes: 1 addition & 6 deletions spec/integrations/rpc/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
end

it "logs access message (rejected)", log: :info do
expect { subject }.to output(/Finished "\/cable\?token=123" \[AnyCable\].*\(Rejected\)/).to_stdout_from_any_process
expect { subject }.to output(/Finished "\/cable\?token=123" \[AnyCable\]/).to_stdout_from_any_process
end
end
end
Expand Down Expand Up @@ -122,11 +122,6 @@
it "responds with error when accessed from a not allowed origin" do
ActionCable.server.config.allowed_request_origins = "http://anycable.com"
expect(subject).to be_failure
expect(JSON.parse(subject.transmissions.first)).to eq(
"type" => "disconnect",
"reason" => "invalid_request",
"reconnect" => false
)
end
end

Expand Down
20 changes: 9 additions & 11 deletions spec/integrations/rpc/perform_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,15 @@
end
end

if ActionCable::Connection::Base.respond_to?(:rescue_from)
context "with rescue_from" do
let(:log) { ApplicationCable::Connection.events_log }
let(:data) { {action: "chat_with", user_id: -1} }

it "catches error with rescue_from" do
expect { subject }
.to change { log.select { |entry| entry[:source] == "error" }.size }
.by(1)
expect(subject).to be_failure
end
context "with rescue_from" do
let(:log) { ApplicationCable::Connection.events_log }
let(:data) { {action: "chat_with", user_id: -1} }

it "catches error with rescue_from" do
expect { subject }
.to change { log.select { |entry| entry[:source] == "error" }.size }
.by(1)
expect(subject).to be_error
end
end

Expand Down
16 changes: 4 additions & 12 deletions spec/lib/anycable/rails/ext/jwt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,12 @@ def connect
it "rejects with token_expired reason when expired" do
token = AnyCable::JWT.encode({user: user}, expires_at: 1.minute.ago)

req = ActionDispatch::TestRequest.create({"QUERY_STRING" => "joken=#{token}", "PATH_INFO" => "/cable"})
conn = AnyCableTestConnection.allocate

ws = double("websocket")
allow(ws).to receive(:alive?) { true }
expect(ws).to receive(:close)

allow(conn).to receive(:websocket) { ws }

conn.singleton_class.include(ActionCable::Connection::TestConnection)
conn.send(:initialize, req)
expect { connect params: {joken: token} }.to raise_error(AnyCable::JWT::ExpiredSignature)

# now we can re-use the same connection info and call #handle_open directly
conn = self.class.connection_class.new(testserver, socket)
conn.handle_open

expect(conn.transmissions.last["reason"]).to eq "token_expired"
expect(socket.transmissions.last["reason"]).to eq "token_expired"
end
end
20 changes: 2 additions & 18 deletions spec/lib/anycable/rails/ext/signed_streams_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,9 @@ def connection_class
AnyCable.config.streams_secret = was_secret
end

let(:conn) do
req = ActionDispatch::TestRequest.create({"PATH_INFO" => "/cable"})
conn = AnyCableTestConnection.allocate

ws = double("websocket")
allow(ws).to receive(:alive?) { true }
allow(ws).to receive(:close)
allow(conn).to receive(:websocket) { ws }

conn.singleton_class.include(ActionCable::Connection::TestConnection)
conn.send(:initialize, req)
conn
end

before do
allow_any_instance_of(AnyCable::Rails::PubSubChannel).to receive(:stream_from)
end
let(:conn) { connect }

let(:transmission) { conn.transmissions.last }
let(:transmission) { transmissions.last }

let(:user) { User.create!(name: "jack") }

Expand Down

0 comments on commit 20c991c

Please sign in to comment.