From 6eb48cd9341408cb1f349496cd14198eaa895f48 Mon Sep 17 00:00:00 2001 From: Andrey Blinov Date: Thu, 9 May 2024 16:33:35 +0300 Subject: [PATCH 1/5] feat: Anycable --- .github/workflows/main.yml | 9 ++++- Gemfile | 2 +- lib/turbo/train.rb | 1 + lib/turbo/train/anycable_server.rb | 36 +++++++++++++++++++ lib/turbo/train/config.rb | 24 ++++++++++++- lib/turbo/train/test_helper.rb | 18 ++++++++++ lib/turbo/train/train.rb | 8 +++++ lib/turbo/train/version.rb | 2 +- node_modules/.yarn-integrity | 10 ++++++ test/dummy/config/initializers/turbo_train.rb | 29 ++++++++++----- test/train/broadcasts_test.rb | 18 +++++----- 11 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 lib/turbo/train/anycable_server.rb create mode 100644 node_modules/.yarn-integrity diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d6c98c8..3793535 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,8 @@ jobs: FANOUT_FASTLY_KEY: ${{ secrets.FANOUT_FASTLY_KEY }} FANOUT_SERVICE_ID: ${{ secrets.FANOUT_SERVICE_ID }} FANOUT_SERVICE_URL: ${{ secrets.FANOUT_SERVICE_URL }} + ANYCABLE_URL: ${{ secrets.ANYCABLE_URL }} + ANYCABLE_BROADCAST_KEY: ${{ secrets.ANYCABLE_BROADCAST_KEY }} name: ${{ format('Tests (Ruby {0}, Rails {1})', matrix.ruby-version, matrix.rails-version) }} runs-on: ubuntu-latest @@ -56,5 +58,10 @@ jobs: TURBO_TRAIN_TEST_SERVER: fanout run: | bin/test test/**/*_test.rb - + + - name: Run tests [anycable] + env: + TURBO_TRAIN_TEST_SERVER: anycable + run: | + bin/test test/**/*_test.rb diff --git a/Gemfile b/Gemfile index 236a474..c873162 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ group :test do # selenium-webdriver-4.9.1/lib/selenium/webdriver/common/logger.rb:51:in `initialize' # https://github.com/SeleniumHQ/selenium/issues/12013 gem 'selenium-webdriver', '4.9.0' - gem 'webdrivers' + gem 'webdrivers', '= 5.3.0' gem 'sqlite3' end diff --git a/lib/turbo/train.rb b/lib/turbo/train.rb index 74eae97..d4ab709 100644 --- a/lib/turbo/train.rb +++ b/lib/turbo/train.rb @@ -4,6 +4,7 @@ require 'turbo/train/base_server' require 'turbo/train/mercure_server' require 'turbo/train/fanout_server' +require 'turbo/train/anycable_server' require 'turbo/train/test_server' require 'turbo/train/test_helper' require "turbo/train/engine" diff --git a/lib/turbo/train/anycable_server.rb b/lib/turbo/train/anycable_server.rb new file mode 100644 index 0000000..16782f6 --- /dev/null +++ b/lib/turbo/train/anycable_server.rb @@ -0,0 +1,36 @@ +module Turbo + module Train + class AnycableServer < BaseServer + def publish(topics:, data:) + uri = URI(server_config.publish_url) + req = Net::HTTP::Post.new(uri) + req['Content-Type'] = 'application/json' + req['Authorization'] = "Bearer #{server_config.broadcast_key}" + + message = data[:data].gsub("\n", '') + + opts = { + use_ssl: uri.scheme == 'https' + } + + payload = [] + + Array(topics).each do |topic| + payload << { stream: topic, data: message } + end + + req.body = payload.to_json + + opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if configuration.skip_ssl_verification + + Net::HTTP.start(uri.host, uri.port, opts) do |http| + http.request(req) + end + end + + def server_config + configuration.anycable + end + end + end +end \ No newline at end of file diff --git a/lib/turbo/train/config.rb b/lib/turbo/train/config.rb index fc0a2c6..20d7dde 100644 --- a/lib/turbo/train/config.rb +++ b/lib/turbo/train/config.rb @@ -47,13 +47,32 @@ def listen_url(topic, **) end end + class AnycableConfiguration + attr_accessor :anycable_url, :broadcast_key + + def initialize + super + @anycable_url = 'http://localhost:8080' + @broadcast_key = 'test' + end + + def publish_url + "#{@anycable_url}/_broadcast" + end + + def listen_url(topic, **) + "#{@anycable_url}/events?stream=#{Turbo::Train.signed_stream_name(topic)}" + end + end + class Configuration - attr_accessor :skip_ssl_verification, :mercure, :fanout, :default_server + attr_accessor :skip_ssl_verification, :mercure, :fanout, :anycable, :default_server def initialize @skip_ssl_verification = Rails.env.development? || Rails.env.test? @mercure = nil @fanout = nil + @anycable = nil @default_server = :mercure end @@ -65,6 +84,9 @@ def server(server_name) when :fanout @fanout ||= FanoutConfiguration.new yield(@fanout) + when :anycable + @anycable ||= AnycableConfiguration.new + yield(@anycable) else raise ArgumentError, "Unknown server name: #{server_name}" end diff --git a/lib/turbo/train/test_helper.rb b/lib/turbo/train/test_helper.rb index 718192c..214245d 100644 --- a/lib/turbo/train/test_helper.rb +++ b/lib/turbo/train/test_helper.rb @@ -8,6 +8,8 @@ def before_setup Turbo::Train::TestServer.new(Turbo::Train.mercure_server, Turbo::Train.configuration) when :fanout Turbo::Train::TestServer.new(Turbo::Train.fanout_server, Turbo::Train.configuration) + when :anycable + Turbo::Train::TestServer.new(Turbo::Train.anycable_server, Turbo::Train.configuration) else raise "Unknown test server: #{ENV['TURBO_TRAIN_TEST_SERVER']}" end @@ -22,6 +24,8 @@ def after_teardown Turbo::Train.mercure_server when :fanout Turbo::Train.fanout_server + when :anycable + Turbo::Train.anycable_server else raise "Unknown test server: #{ENV['TURBO_TRAIN_TEST_SERVER']}" end @@ -60,6 +64,20 @@ def assert_body_match(r) assert_match "Published\n", r.body elsif Turbo::Train.server.real_server.is_a?(Turbo::Train::MercureServer) assert_match /urn:uuid:.*/, r.body + elsif Turbo::Train.server.real_server.is_a?(Turbo::Train::AnycableServer) + assert_match '', r.body + else + raise "Unknown server type" + end + end + + def assert_code_ok(r) + if Turbo::Train.server.real_server.is_a?(Turbo::Train::FanoutServer) + assert_equal r.code, '200' + elsif Turbo::Train.server.real_server.is_a?(Turbo::Train::MercureServer) + assert_equal r.code, '200' + elsif Turbo::Train.server.real_server.is_a?(Turbo::Train::AnycableServer) + assert_equal r.code, '201' else raise "Unknown server type" end diff --git a/lib/turbo/train/train.rb b/lib/turbo/train/train.rb index f8f0cc0..2ee2c06 100644 --- a/lib/turbo/train/train.rb +++ b/lib/turbo/train/train.rb @@ -24,6 +24,8 @@ def server(server = nil) mercure_server when :fanout fanout_server + when :anycable + anycable_server else raise ArgumentError, "Unknown server: #{server}" end @@ -41,6 +43,12 @@ def fanout_server @fanout_server ||= FanoutServer.new(configuration) end + def anycable_server + raise ArgumentError, "Anycable configuration is missing" unless configuration.anycable + + @anycable_server ||= AnycableServer.new(configuration) + end + def stream_name_from(streamables) if streamables.is_a?(Array) streamables.map { |streamable| stream_name_from(streamable) }.join(":") diff --git a/lib/turbo/train/version.rb b/lib/turbo/train/version.rb index 995ccd2..7fbc953 100644 --- a/lib/turbo/train/version.rb +++ b/lib/turbo/train/version.rb @@ -1,5 +1,5 @@ module Turbo module Train - VERSION = "0.3.0" + VERSION = "0.4.0" end end diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity new file mode 100644 index 0000000..9937b9e --- /dev/null +++ b/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "darwin-arm64-93", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/test/dummy/config/initializers/turbo_train.rb b/test/dummy/config/initializers/turbo_train.rb index b0d645d..c9e4530 100644 --- a/test/dummy/config/initializers/turbo_train.rb +++ b/test/dummy/config/initializers/turbo_train.rb @@ -1,15 +1,26 @@ Turbo::Train.configure do |config| config.default_server = ENV.fetch('TURBO_TRAIN_TEST_SERVER', 'mercure').to_sym - config.server :mercure do |mercure| - mercure.mercure_domain = ENV['MERCURE_DOMAIN'] || raise('MERCURE_DOMAIN is not set') - mercure.publisher_key = ENV['MERCURE_PUBLISHER_KEY'] || raise('MERCURE_PUBLISHER_KEY is not set') - mercure.subscriber_key = ENV['MERCURE_SUBSCRIBER_KEY'] || raise('MERCURE_SUBSCRIBER_KEY is not set') + if ENV.fetch('TURBO_TRAIN_TEST_SERVER', 'mercure').to_sym == :mercure + config.server :mercure do |mercure| + mercure.mercure_domain = ENV['MERCURE_DOMAIN'] || raise('MERCURE_DOMAIN is not set') + mercure.publisher_key = ENV['MERCURE_PUBLISHER_KEY'] || raise('MERCURE_PUBLISHER_KEY is not set') + mercure.subscriber_key = ENV['MERCURE_SUBSCRIBER_KEY'] || raise('MERCURE_SUBSCRIBER_KEY is not set') + end end - config.server :fanout do |fanout| - fanout.service_url = ENV['FANOUT_SERVICE_URL'] || raise('FANOUT_SERVICE_URL is not set') - fanout.service_id = ENV['FANOUT_SERVICE_ID'] || raise('FANOUT_SERVICE_ID is not set') - fanout.fastly_key = ENV['FANOUT_FASTLY_KEY'] || raise('FANOUT_FASTLY_KEY is not set') + if ENV.fetch('TURBO_TRAIN_TEST_SERVER', 'mercure').to_sym == :fanout + config.server :fanout do |fanout| + fanout.service_url = ENV['FANOUT_SERVICE_URL'] || raise('FANOUT_SERVICE_URL is not set') + fanout.service_id = ENV['FANOUT_SERVICE_ID'] || raise('FANOUT_SERVICE_ID is not set') + fanout.fastly_key = ENV['FANOUT_FASTLY_KEY'] || raise('FANOUT_FASTLY_KEY is not set') + end end -end + + if ENV.fetch('TURBO_TRAIN_TEST_SERVER', 'mercure').to_sym == :anycable + config.server :anycable do |anycable| + anycable.anycable_url = ENV['ANYCABLE_URL'] || raise('ANYCABLE_URL is not set') + anycable.broadcast_key = ENV['ANYCABLE_BROADCAST_KEY'] || raise('ANYCABLE_BROADCAST_KEY is not set') + end + end +end \ No newline at end of file diff --git a/test/train/broadcasts_test.rb b/test/train/broadcasts_test.rb index 6aff97d..a6921d4 100644 --- a/test/train/broadcasts_test.rb +++ b/test/train/broadcasts_test.rb @@ -11,7 +11,7 @@ class BroadcastsTest < ActiveSupport::TestCase assert_broadcast_on "messages", turbo_stream_action_tag("replace", target: "message_1", template: "Goodbye!") do r = Turbo::Train.broadcast_render_to("messages", partial: 'messages/message', locals: { message: message }) - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -19,7 +19,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_action_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("replace", target: "target", template: 'content') do r = Turbo::Train.broadcast_action_to('messages', action: 'replace', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -27,7 +27,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_append_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("append", target: "target", template: 'content') do r = Turbo::Train.broadcast_append_to('messages', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -35,7 +35,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_remove_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("remove", target: "target") do r = Turbo::Train.broadcast_remove_to('messages', target: 'target') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -43,7 +43,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_replace_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("replace", target: "target", template: 'content') do r = Turbo::Train.broadcast_replace_to('messages', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -51,7 +51,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_update_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("update", target: "target", template: 'content') do r = Turbo::Train.broadcast_update_to('messages', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -59,7 +59,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_before_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("before", target: "target", template: 'content') do r = Turbo::Train.broadcast_before_to('messages', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -67,7 +67,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_after_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("after", target: "target", template: 'content') do r = Turbo::Train.broadcast_after_to('messages', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end @@ -75,7 +75,7 @@ class BroadcastsTest < ActiveSupport::TestCase test "broadcast_prepend_to" do assert_broadcast_on 'messages', turbo_stream_action_tag("prepend", target: "target", template: 'content') do r = Turbo::Train.broadcast_prepend_to('messages', target: 'target', content: 'content') - assert_equal r.code, '200' + assert_code_ok(r) assert_body_match(r) end end From c4b10c838990490ed9f99b4176fc50d7555092a4 Mon Sep 17 00:00:00 2001 From: Andrey Blinov Date: Thu, 9 May 2024 16:45:57 +0300 Subject: [PATCH 2/5] fix: selenium --- Gemfile | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index c873162..e075af9 100644 --- a/Gemfile +++ b/Gemfile @@ -21,15 +21,9 @@ end group :test do gem 'puma' - gem 'capybara' + gem 'capybara', '>= 3.39.2' gem 'rexml' - # Locked because on 4.9.1 getting error: - # BroadcastingTest#test_Turbo::Train_broadcasts_Turbo_Streams: - # ArgumentError: wrong number of arguments (given 2, expected 0..1) - # selenium-webdriver-4.9.1/lib/selenium/webdriver/common/logger.rb:51:in `initialize' - # https://github.com/SeleniumHQ/selenium/issues/12013 - gem 'selenium-webdriver', '4.9.0' - gem 'webdrivers', '= 5.3.0' + gem 'selenium-webdriver', '4.20.0' gem 'sqlite3' end From cf9a750e4301ebbee2c45a22ab376c83e0eb7596 Mon Sep 17 00:00:00 2001 From: Andrey Blinov Date: Thu, 9 May 2024 17:08:45 +0300 Subject: [PATCH 3/5] fix: CI --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index e075af9..2447e89 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ group :test do gem 'capybara', '>= 3.39.2' gem 'rexml' gem 'selenium-webdriver', '4.20.0' - gem 'sqlite3' + gem 'sqlite3', '~> 1.4' end # Start debugger with binding.b [https://github.com/ruby/debug] From 642eef69c5f80e11a9e1ba1b4169fe5d8261b355 Mon Sep 17 00:00:00 2001 From: Andrey Blinov Date: Thu, 9 May 2024 17:15:00 +0300 Subject: [PATCH 4/5] fix: CI --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3793535..40330e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,8 +5,7 @@ jobs: strategy: matrix: ruby-version: - - "2.7" - - "3.0" + - "3.1" - "3.2.2" rails-version: - "6.1" From b7cafe9ce19892a0c4e8415c2bfaf047a04e4cd3 Mon Sep 17 00:00:00 2001 From: Andrey Blinov Date: Mon, 13 May 2024 15:24:14 +0300 Subject: [PATCH 5/5] fix: README --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dea294a..7d82abd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ src="https://user-images.githubusercontent.com/3010927/210603861-4b265489-a4a7-4d2a-bceb-40ceccebcd96.jpg"> -Real-time page updates for your Rails app over SSE with [Mercure](https://mercure.rocks) or [Fanout Cloud](https://fanout.io/cloud) and [Hotwire Turbo](https://turbo.hotwired.dev/handbook/streams#integration-with-server-side-frameworks). +Real-time page updates for your Rails app over SSE with [Mercure](https://mercure.rocks), [Fanout Cloud](https://fanout.io/cloud) or [AnyCable](https://anycable.io/) and [Hotwire Turbo](https://turbo.hotwired.dev/handbook/streams#integration-with-server-side-frameworks). * **Uses [SSE](https://html.spec.whatwg.org/multipage/server-sent-events.html)**. No more websockets, client libraries, JS code and handling reconnects. Just an HTTP connection. Let the [browser](https://caniuse.com/eventsource) do the work. * **Seamless Hotwire integration.** Use it exactly like [ActionCable](https://github.com/hotwired/turbo-rails#come-alive-with-turbo-streams). Drop-in replacement for `broadcast_action_to` and usual helpers. @@ -66,6 +66,14 @@ We only support the cloud version today. To use [Fanout](https://fanout.io/cloud Coming soon. +#### AnyCable + +``` +anycable-go --host=localhost --port=8080 --sse --broadcast_adapter=http --broadcast_key=test --public_streams --noauth +``` + +Coming soon. + ## Usage If you are familiar with broadcasting from ActionCable, usage would be extremely familiar: @@ -124,6 +132,11 @@ Turbo::Train.configure do |config| fanout.service_id = ... fanout.fastly_key = ... end + + config.server :anycable do |fanout| + ac.anycable_url = 'http://0.0.0.0:8080' + ac.broadcast_key = 'test' + end end ```