Skip to content

Commit f3202ae

Browse files
committed
refactor and test
1 parent 3181dd2 commit f3202ae

26 files changed

+401
-79
lines changed

.rubocop_todo.yml

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2024-05-28 13:58:15 UTC using RuboCop version 1.64.0.
3+
# on 2024-06-12 14:52:30 UTC using RuboCop version 1.64.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -9,22 +9,47 @@
99
# Offense count: 1
1010
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
1111
Metrics/AbcSize:
12-
Max: 26
12+
Max: 21
1313

1414
# Offense count: 1
1515
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
1616
Metrics/MethodLength:
17-
Max: 24
17+
Max: 20
18+
19+
# Offense count: 4
20+
RSpec/AnyInstance:
21+
Exclude:
22+
- 'spec/redis_connections/application_redis_connection_spec.rb'
23+
- 'spec/redis_connections/bridge_redis_connection_spec.rb'
1824

1925
# Offense count: 1
2026
# Configuration parameters: IgnoredMetadata.
2127
RSpec/DescribeClass:
2228
Exclude:
2329
- 'spec/browser/restricted_area_spec.rb'
2430

25-
# Offense count: 3
26-
# Configuration parameters: AllowedConstants.
27-
Style/Documentation:
31+
# Offense count: 6
32+
# Configuration parameters: CountAsOne.
33+
RSpec/ExampleLength:
34+
Max: 20
35+
36+
# Offense count: 2
37+
# Configuration parameters: EnforcedStyle.
38+
# SupportedStyles: have_received, receive
39+
RSpec/MessageSpies:
40+
Exclude:
41+
- 'spec/redis_connections/application_redis_connection_spec.rb'
42+
- 'spec/redis_connections/bridge_redis_connection_spec.rb'
43+
44+
# Offense count: 2
45+
RSpec/MultipleExpectations:
46+
Max: 2
47+
48+
# Offense count: 8
49+
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
50+
RSpec/VerifiedDoubles:
2851
Exclude:
29-
- 'app/controllers/application_controller.rb'
30-
- 'app/controllers/websocket_controller.rb'
52+
- 'spec/redis_connections/application_redis_connection_spec.rb'
53+
- 'spec/redis_connections/bridge_redis_connection_spec.rb'
54+
- 'spec/services/async_redis_service_spec.rb'
55+
- 'spec/sockets/chat_socket_spec.rb'

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ gem 'async-websocket'
5656
gem 'thread-local'
5757

5858
group :development, :test do
59+
gem 'async-rspec'
5960
gem 'factory_bot_rails'
6061
gem 'pry-byebug', platforms: %i[mri mingw x64_mingw]
6162
gem 'rspec-rails'

Gemfile.lock

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ GEM
119119
io-endpoint (~> 0.10)
120120
io-stream (~> 0.4)
121121
protocol-redis (~> 0.8.0)
122+
async-rspec (1.17.0)
123+
rspec (~> 3.0)
124+
rspec-files (~> 1.0)
125+
rspec-memory (~> 1.0)
122126
async-service (0.12.0)
123127
async
124128
async-container (~> 0.16)
@@ -447,11 +451,19 @@ GEM
447451
railties (>= 5.2)
448452
rexml (3.3.0)
449453
strscan
454+
rspec (3.13.0)
455+
rspec-core (~> 3.13.0)
456+
rspec-expectations (~> 3.13.0)
457+
rspec-mocks (~> 3.13.0)
450458
rspec-core (3.13.0)
451459
rspec-support (~> 3.13.0)
452460
rspec-expectations (3.13.0)
453461
diff-lcs (>= 1.2.0, < 2.0)
454462
rspec-support (~> 3.13.0)
463+
rspec-files (1.1.3)
464+
rspec (~> 3.0)
465+
rspec-memory (1.0.4)
466+
rspec (~> 3.0)
455467
rspec-mocks (3.13.1)
456468
diff-lcs (>= 1.2.0, < 2.0)
457469
rspec-support (~> 3.13.0)
@@ -587,6 +599,7 @@ DEPENDENCIES
587599
ar-uuid
588600
async-io
589601
async-redis
602+
async-rspec
590603
async-websocket
591604
bcrypt (~> 3.1.19)
592605
bootsnap (>= 1.1.0)

app/channels/application_cable/channel.rb

Lines changed: 0 additions & 6 deletions
This file was deleted.

app/channels/application_cable/connection.rb

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'async/websocket/adapters/rails'
4-
5-
require 'async/redis'
6-
require 'thread/local'
7-
8-
module Chat
9-
# Websocket control class
10-
module Redis
11-
extend Thread::Local
12-
13-
def self.local
14-
endpoint = ::IO::Endpoint.tcp('redis', 6379)
15-
Async::Redis::Client.new(endpoint)
16-
end
17-
end
18-
end
19-
203
# Entrypoint for all websocket connections to this server
214
class WebsocketController < ApplicationController
225
def index; end
@@ -26,34 +9,6 @@ def index; end
269
def connect
2710
channel = params.fetch(:channel, 'chat.general')
2811

29-
self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
30-
Sync do
31-
redisclient = Chat::Redis.instance
32-
subscription_task = Async do
33-
# Subscribe to the channel and broadcast incoming messages:
34-
redisclient.subscribe(channel) do |context|
35-
loop do
36-
_type, _name, message = context.listen
37-
38-
# The message is text, but contains JSON.
39-
connection.send_text(message)
40-
connection.flush
41-
end
42-
end
43-
end
44-
45-
# Perpetually read incoming messages and publish them to Redis:
46-
while (message = connection.read)
47-
logger.info "Messaged received => #{message.buffer}"
48-
redisclient.publish(channel, message.buffer)
49-
end
50-
rescue Protocol::WebSocket::ClosedError
51-
logger.info 'Websocket closed'
52-
# Ignore.
53-
ensure
54-
logger.info "Subscription #{subscription_task} stopped"
55-
subscription_task&.stop
56-
end
57-
end
12+
self.response = ChatSocket.new(channel).open(request)
5813
end
5914
end

app/jobs/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Jobs
2+
----
3+
4+
This is where you put your job classes. All workers must be named `something_job.rb`.
5+
6+
To make a new kind of job just run the generator:
7+
8+
`rails generate job Something`
9+
10+
This will create
11+
12+
```
13+
class SomethingJob < ApplicationJob
14+
queue_as :default
15+
16+
def perform(*args)
17+
end
18+
end
19+
```
20+
21+
To run any job just run `SomethingJob.perform_later(params)`.
22+
23+
More info:
24+
25+
https://github.com/sidekiq/sidekiq/wiki/Active-Job
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
# Generic Redis Connection for applications to use
4+
class ApplicationRedisConnection
5+
def initialize(channel)
6+
@channel = channel
7+
async_task
8+
end
9+
10+
def on_message(msg_type, msg_name, msg_content); end
11+
12+
def publish(message)
13+
async_redis.publish(@channel, message)
14+
end
15+
16+
def close
17+
return if @async_task.nil?
18+
19+
@async_task&.stop
20+
@async_task = nil
21+
end
22+
23+
private
24+
25+
def async_task
26+
@async_task ||= Async do
27+
async_redis.subscribe(@channel) do |context|
28+
loop do
29+
msg_type, msg_name, msg_content = context.listen
30+
on_message(msg_type, msg_name, msg_content)
31+
end
32+
end
33+
end
34+
end
35+
36+
def async_redis
37+
AsyncRedisService.default
38+
end
39+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
# class to accept messages from redis channel and send it to the delegate
4+
class BridgeRedisConnection < ApplicationRedisConnection
5+
def initialize(channel, delegate)
6+
@delegate = delegate
7+
super(channel)
8+
end
9+
10+
def on_message(msg_type, msg_name, msg_content)
11+
Rails.logger.debug { "redis message: #{msg_type}, #{msg_name}, #{msg_content}" }
12+
return unless @delegate.respond_to?(:redis_message)
13+
14+
Rails.logger.debug 'send redis message'
15+
@delegate&.redis_message(msg_content)
16+
end
17+
end

app/services/async_redis_service.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
require 'async/redis'
4+
require 'thread/local'
5+
6+
# Asynchronous Redis Client for pub sub purposes
7+
module AsyncRedisService
8+
extend Thread::Local
9+
10+
def self.default
11+
uri = URI.parse(default_redis_url)
12+
endpoint = ::IO::Endpoint.tcp(uri.host, uri.port)
13+
Async::Redis::Client.new(endpoint)
14+
end
15+
16+
def self.default_redis_url
17+
ENV.fetch('REDIS_URL', nil)
18+
end
19+
end

app/sockets/application_socket.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require 'async/websocket/adapters/rails'
4+
5+
# Generic socket system for the application
6+
class ApplicationSocket
7+
attr_reader :connection
8+
9+
def open(request)
10+
Async::WebSocket::Adapters::Rails.open(request) do |connection|
11+
Sync do
12+
Rails.logger.debug { "Connection #{connection} opened" }
13+
@connection = connection
14+
on_open(connection)
15+
16+
while (message = connection.read)
17+
on_message(message)
18+
end
19+
rescue Protocol::WebSocket::ClosedError
20+
Rails.logger.debug { "Connection #{connection} closed" }
21+
on_closed
22+
rescue StandardError => e
23+
Rails.logger.debug { "Connection #{connection} errored" }
24+
Rails.logger.debug e
25+
Rails.logger.debug e.backtrace
26+
on_error(e)
27+
ensure
28+
Rails.logger.debug { "Connection #{connection} stopped" }
29+
end
30+
end
31+
end
32+
33+
def on_message(message); end
34+
35+
def on_open(connection); end
36+
37+
def on_closed; end
38+
39+
def on_error(err); end
40+
end

app/sockets/chat_socket.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
# websocket handler for chat application
4+
class ChatSocket < ApplicationSocket
5+
def initialize(channel_name)
6+
@rc = BridgeRedisConnection.new(channel_name, self)
7+
super()
8+
end
9+
10+
def on_message(message)
11+
Rails.logger.debug { "message callback: #{message.buffer}" }
12+
@rc.publish(message.buffer)
13+
end
14+
15+
def redis_message(message)
16+
Rails.logger.debug { "redis receive: #{message}" }
17+
connection.send_text(message)
18+
connection.flush
19+
rescue StandardError => e
20+
Rails.logger.debug { "error: #{e.message}" }
21+
@rc.close
22+
end
23+
end

app/workers/README.md

Lines changed: 0 additions & 10 deletions
This file was deleted.

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
sessions: 'admins/sessions'
99
}
1010

11+
# remove these if you do not want the websocket endpoints
1112
get 'websocket', to: 'websocket#index'
1213
match 'connect', to: 'websocket#connect', via: %i[get connect]
1314

spec/apis/v1/health_api_spec.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'rails_helper'
4-
53
RSpec.describe HealthApi, type: :request do
64
it 'returns ok' do
75
get '/api/v1/health'

spec/browser/restricted_area_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'rails_helper'
43
require 'browser_helper'
54

65
RSpec.describe 'Restricted Area', :js, type: :browser do

0 commit comments

Comments
 (0)