diff --git a/.dockerdev/.bashrc b/.dockerdev/.bashrc deleted file mode 100644 index 4f233c53..00000000 --- a/.dockerdev/.bashrc +++ /dev/null @@ -1 +0,0 @@ -alias be="bundle exec" diff --git a/.dockerdev/.pryrc b/.dockerdev/.pryrc deleted file mode 100644 index 4d4a34ba..00000000 --- a/.dockerdev/.pryrc +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -if ENV["HISTFILE"] - hist_dir = ENV["HISTFILE"].sub(/\/[^\/]+$/, "") - Pry.config.history_save = true - Pry.config.history_file = File.join(hist_dir, ".pry_history") -end diff --git a/.dockerdev/.psqlrc b/.dockerdev/.psqlrc deleted file mode 100644 index 57ea736f..00000000 --- a/.dockerdev/.psqlrc +++ /dev/null @@ -1,26 +0,0 @@ --- Don't display the "helpful" message on startup. -\set QUIET 1 - --- Allow specifying the path to history file via `PSQL_HISTFILE` env variable --- (and fallback to the default $HOME/.psql_history otherwise) -\set HISTFILE `[[ -z $PSQL_HISTFILE ]] && echo $HOME/.psql_history || echo $PSQL_HISTFILE` - --- Show how long each query takes to execute -\timing - --- Use best available output format -\x auto - --- Verbose error reports -\set VERBOSITY verbose - --- If a command is run more than once in a row, --- only store it once in the history -\set HISTCONTROL ignoredups -\set COMP_KEYWORD_CASE upper - --- By default, NULL displays as an empty space. Is it actually an empty --- string, or is it null? This makes that distinction visible -\pset null '[NULL]' - -\unset QUIET diff --git a/.dockerdev/Aptfile b/.dockerdev/Aptfile deleted file mode 100644 index f027e0d4..00000000 --- a/.dockerdev/Aptfile +++ /dev/null @@ -1 +0,0 @@ -vim diff --git a/.dockerdev/Dockerfile b/.dockerdev/Dockerfile deleted file mode 100644 index 686c2396..00000000 --- a/.dockerdev/Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -ARG RUBY_VERSION -FROM ruby:$RUBY_VERSION-slim-buster - -ARG PG_MAJOR -ARG NODE_MAJOR -ARG BUNDLER_VERSION -ARG YARN_VERSION - -# Common dependencies -RUN apt-get update -qq \ - && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ - build-essential \ - gnupg2 \ - curl \ - less \ - git \ - && apt-get clean \ - && rm -rf /var/cache/apt/archives/* \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && truncate -s 0 /var/log/*log - -# Add PostgreSQL to sources list -RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ - && echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list - -# Add NodeJS to sources list -RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - - -# Add Yarn to the sources list -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list - -# Install dependencies -COPY .dockerdev/Aptfile /tmp/Aptfile -RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \ - DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ - libpq-dev \ - postgresql-client-$PG_MAJOR \ - nodejs \ - yarn=$YARN_VERSION-1 \ - $(cat /tmp/Aptfile | xargs) && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ - truncate -s 0 /var/log/*log - -# Configure bundler -ENV LANG=C.UTF-8 \ - BUNDLE_JOBS=4 \ - BUNDLE_RETRY=3 - -# Uncomment this line if you store Bundler settings in the project's root -# ENV BUNDLE_APP_CONFIG=.bundle - -# Uncomment this line if you want to run binstubs without prefixing with `bin/` or `bundle exec` -# ENV PATH /app/bin:$PATH - -# Upgrade RubyGems and install required Bundler version -# See https://github.com/evilmartians/terraforming-rails/pull/24 for discussion -RUN gem update --system && \ - rm /usr/local/lib/ruby/gems/*/specifications/default/bundler-*.gemspec && \ - gem uninstall bundler && \ - gem install bundler -v $BUNDLER_VERSION - -# Create a directory for the app code -RUN mkdir -p /app - -WORKDIR /app diff --git a/.github/workflows/system_test.yml b/.github/workflows/system_test.yml index a6c2a6ed..f5bece29 100644 --- a/.github/workflows/system_test.yml +++ b/.github/workflows/system_test.yml @@ -13,24 +13,7 @@ jobs: BUNDLE_JOBS: 4 BUNDLE_RETRY: 3 CI: true - DATABASE_URL: postgres://postgres:postgres@localhost:5432 - CHROME_URL: http://localhost:3000 RAILS_ENV: test - services: - postgres: - image: postgres:13.0 - env: - POSTGRES_PASSWORD: postgres - ports: ["5432:5432"] - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - redis: - image: redis:5.0-alpine - ports: ["6379:6379"] - options: --health-cmd="redis-cli ping" --health-interval 1s --health-timeout 3s --health-retries 30 - chrome: - image: browserless/chrome:1.31-chrome-stable - ports: ["3000:3000"] - options: -e CONNECTION_TIMEOUT=600000 steps: - uses: actions/checkout@v2 - uses: actions/cache@v1 @@ -42,9 +25,10 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 3.0.1 - - name: Install PostgreSQL client + - name: Install libsqlite3 run: | - sudo apt-get -yqq install libpq-dev + sudo apt-get update + sudo apt-get install libsqlite3-dev - name: Bundle install run: | bundle config path /home/runner/bundle @@ -69,8 +53,5 @@ jobs: - name: Run RSpec run: | bundle exec rails db:prepare - bundle exec rails g anycable:download --bin-path=./bin bundle exec rails webpacker:compile - ./bin/anycable-go --host "0.0.0.0" & - export CABLE_URL="ws://$(hostname):8080/cable" bundle exec rspec --tag type:system diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 599d2122..0239adb9 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -13,19 +13,7 @@ jobs: BUNDLE_JOBS: 4 BUNDLE_RETRY: 3 CI: true - DATABASE_URL: postgres://postgres:postgres@localhost:5432 RAILS_ENV: test - services: - postgres: - image: postgres:11.1 - ports: ["5432:5432"] - env: - POSTGRES_PASSWORD: postgres - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - redis: - image: redis:5.0-alpine - ports: ["6379:6379"] - options: --health-cmd="redis-cli ping" --health-interval 1s --health-timeout 3s --health-retries 30 steps: - uses: actions/checkout@v2 - uses: actions/cache@v1 @@ -37,9 +25,10 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: 3.0.1 - - name: Install PostgreSQL client + - name: Install libsqlite3 run: | - sudo apt-get -yqq install libpq-dev + sudo apt-get update + sudo apt-get install libsqlite3-dev - name: Bundle install run: | bundle config path /home/runner/bundle diff --git a/Gemfile b/Gemfile index 4b1db13b..8406e1bc 100644 --- a/Gemfile +++ b/Gemfile @@ -5,9 +5,8 @@ ruby '3.0.1' gem 'rails', '~> 6.1' gem 'webpacker', '6.0.0.beta.6' -gem 'pg', '~> 1.0' +gem 'sqlite3', '~> 1.4' gem 'puma', '~> 4.1' -gem 'redis', '~> 4.0' gem 'anycable', '> 1.0.99' gem 'anycable-rails', '> 1.0.99' @@ -21,6 +20,10 @@ group :development, :test do gem 'pry-byebug' gem 'pry-rails' gem 'rspec-rails', '~> 4.0.0' + + # Rack-based AnyCable server implementation + gem 'anycable-rack-server', '~> 0.4.0' + gem 'msgpack', '~> 1.4' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index aad7a2b7..ac78948c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,6 +76,11 @@ GEM anycable-core (1.1.0.rc1) anyway_config (>= 2.1.0) google-protobuf (>= 3.13) + anycable-rack-server (0.4.0) + anycable (> 1.0.99, < 2.0) + anyway_config (>= 2.1.0) + connection_pool (~> 2.2) + websocket (~> 1.2) anycable-rails (1.1.0.rc1.1) actioncable (>= 6) anycable-core (> 1.0.99, < 2.0) @@ -101,6 +106,7 @@ GEM adamantium (~> 0.2.0) equalizer (~> 0.0.9) concurrent-ruby (1.1.8) + connection_pool (2.2.5) crass (1.0.6) cuprite (0.13) capybara (>= 2.1, < 4) @@ -156,7 +162,6 @@ GEM parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) - pg (1.2.3) procto (0.0.3) pry (0.13.1) coderay (~> 1.1) @@ -206,7 +211,6 @@ GEM rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) - redis (4.2.5) regexp_parser (2.1.1) rexml (3.2.4) rspec-core (3.10.1) @@ -263,6 +267,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + sqlite3 (1.4.2) standard (1.0.4) rubocop (= 1.11.0) rubocop-performance (= 1.10.1) @@ -289,6 +294,7 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) + websocket (1.2.9) websocket-driver (0.7.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -301,22 +307,23 @@ PLATFORMS DEPENDENCIES anycable (> 1.0.99) + anycable-rack-server (~> 0.4.0) anycable-rails (> 1.0.99) bootsnap (>= 1.4.2) capybara cuprite listen + msgpack (~> 1.4) nanoid - pg (~> 1.0) pry-byebug pry-rails puma (~> 4.1) rails (~> 6.1) - redis (~> 4.0) rspec-rails (~> 4.0.0) rubocop-rails rubocop-rspec ruby-next (>= 0.10.0) + sqlite3 (~> 1.4) standard (~> 1.0) test-prof turbo-rails diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 00000000..8d13a6d3 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +server: bundle exec rails s -p 3000 +assets: bundle exec bin/webpack-dev-server diff --git a/README.md b/README.md index a837daed..3d821d19 100644 --- a/README.md +++ b/README.md @@ -12,59 +12,38 @@ This repository contains the code for AnyCable Rails demo application and its di ## Installation -This app has a Docker-first configuration based one the [Ruby on Whales post](https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development). +This branch has a minimal number of dependencies to make it easier to run the project locally: SQLite instead of Postgres, no Redis, no Docker. -You need: - -- `docker` and `docker-compose` installed. - -For MacOS just use [official app](https://docs.docker.com/engine/installation/mac/). - -- [`dip`](https://github.com/bibendi/dip) installed. - -Run the following command to build images and provision the application: +You need Ruby and NodeJS installed on your local machine (and maybe some common system deps for such gems as nokogiri and sqlite3). ```sh -dip provision +bundle install +yarn install +bin/setup ``` ## Running -You can start Rails server along with AnyCable by running: +You can start Rails server by running: ```sh -dip up rails anycable +bundle exec rails s ``` Then go to [http://localhost:3000/](http://localhost:3000/) and see the application in action. -## Debugging - -If you want to run Rails server and/or with debugging capabilites, run the following commands: - -```sh -# for Rails server -dip rails s - -# for AnyCable -dip anycable -``` - ## Testing -We separate unit and system specs and provide convenient Dip commands to run them: +We use RSpec for testing: ```sh -# only unit tests -dip rspec - -# only system tests -dip rspec system +bundle exec rspec ``` +**NOTE:** System tests require Chrome browser to be installed on your machine (no other software required). + ## Resources -- [Ruby on Whales](https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development)—learn about the Docker development setup used for this application. - [RuboCoping with legacy](https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard)—this is how we configure RuboCop. - [Evil Front](https://evilmartians.com/chronicles/evil-front-part-3)—some frontend ideas are borrowed from this post. - [Ruby Next](https://evilmartians.com/chronicles/ruby-next-make-all-rubies-quack-alike)—we're using the edge Ruby syntax! diff --git a/config.ru b/config.ru index bff88d60..2e030846 100644 --- a/config.ru +++ b/config.ru @@ -5,3 +5,4 @@ require_relative "config/environment" run Rails.application +Rails.application.load_server diff --git a/config/anycable.yml b/config/anycable.yml index 062de4e2..11c929a4 100644 --- a/config/anycable.yml +++ b/config/anycable.yml @@ -20,14 +20,22 @@ default: &default rpc_host: "127.0.0.1:50051" # Whether to enable gRPC level logging or not log_grpc: false - # Use Redis to broadcast messages to AnyCable server - broadcast_adapter: redis + # Use HTTP adapter for a quick start (since redis gem is not present in the project) + broadcast_adapter: http # Use the same channel name for WebSocket server, e.g.: # $ anycable-go --redis-channel="__anycable__" redis_channel: "__anycable__" development: <<: *default + embedded: true + http_broadcast_url: "http://localhost:3000/_anycable_rack_broadcast" + +test: + <<: *default + http_broadcast_url: "http://localhost:3023/_anycable_rack_broadcast" production: <<: *default + # Use Redis in production + broadcast_adapter: redis diff --git a/config/database.yml b/config/database.yml index dfc41703..fb83db45 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,16 +1,16 @@ default: &default - adapter: postgresql + adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> url: <%= ENV["DATABASE_URL"] %> timeout: 5000 development: <<: *default - database: any_rails_demo_dev + database: db/any_rails_demo_dev.sqlite3 test: <<: *default - database: any_rails_demo_test + database: db/any_rails_demo_test.sqlite3 production: <<: *default diff --git a/config/environments/development.rb b/config/environments/development.rb index 4b79746e..9dd2794f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -2,9 +2,14 @@ Rails.application.configure do # Specify AnyCable WebSocket server URL to use by JS client - config.after_initialize do - config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "ws://localhost:8080/cable") if AnyCable::Rails.enabled? - end + config.action_cable.url = ENV.fetch("CABLE_URL", "/rack_cable") + # Disable built-in Action Cable + config.action_cable.mount_path = nil + # Run AnyCable Rack server at a custom path + config.any_cable_rack.mount_path = "/rack_cable" + # Use Msgpack coder + config.any_cable_rack.coder = :msgpack + # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on diff --git a/config/environments/test.rb b/config/environments/test.rb index c4e9997a..ecdd9c5f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,13 +8,14 @@ Rails.application.configure do # Enforce AnyCable patches to make it possible to use any_cable adapter for system tests require "anycable/rails/actioncable/connection" - # Specify AnyCable WebSocket server URL to use by JS client - config.after_initialize do - config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "ws://localhost:8080/cable") - end - + config.action_cable.url = "/rack_cable" + # Disable built-in Action Cable config.action_cable.mount_path = nil + # Run AnyCable Rack server at a custom path + config.any_cable_rack.mount_path = "/rack_cable" + # Use Msgpack coder + config.any_cable_rack.coder = :msgpack # Settings specified here will take precedence over those in config/application.rb. config.cache_classes = true diff --git a/db/schema.rb b/db/schema.rb index 92f5ed04..e9f39cf7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,11 +12,8 @@ ActiveRecord::Schema.define(version: 2020_05_04_092152) do - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - create_table "items", force: :cascade do |t| - t.bigint "list_id", null: false + t.integer "list_id", null: false t.text "desc", null: false t.boolean "completed", default: false, null: false t.datetime "created_at", precision: 6, null: false @@ -26,7 +23,7 @@ create_table "lists", force: :cascade do |t| t.string "name", null: false - t.bigint "workspace_id", null: false + t.integer "workspace_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["workspace_id"], name: "index_lists_on_workspace_id" diff --git a/dip.yml b/dip.yml deleted file mode 100644 index 38532c32..00000000 --- a/dip.yml +++ /dev/null @@ -1,91 +0,0 @@ -version: '5.0' - -environment: - RAILS_ENV: development - -compose: - files: - - docker-compose.yml - -interaction: - runner: - description: Open a Bash shell within a Rails container (with dependencies up) - service: rails - command: /bin/bash - - bash: - description: Run an arbitrary script within a container (or open a shell without deps) - service: rails - command: /bin/bash - compose_run_options: [no-deps] - - bundle: - description: Run Bundler commands - service: rails - command: bundle - compose_run_options: [no-deps] - - rake: - description: Run Rake commands - service: rails - command: bundle exec rake - - rails: - description: Run Rails commands - service: rails - command: bundle exec rails - subcommands: - s: - description: Run Rails server available at http://localhost:3030 - service: rails - compose: - run_options: [service-ports, use-aliases] - - anycable: - description: Run AnyCable RPC (with debugging capabilities) - service: anycable - compose_run_options: [service-ports, use-aliases] - - yarn: - description: Run Yarn commands - service: rails - command: yarn - compose_run_options: [no-deps] - - rspec: - description: Run Rails unit tests - service: rails - environment: - RAILS_ENV: test - command: bundle exec rspec --exclude-pattern spec/system/**/*_spec.rb - subcommands: - system: - description: Run Rails system tests - service: rspec_system - command: bundle exec rspec --pattern spec/system/**/*_spec.rb - compose: - run_options: [service-ports, use-aliases] - - rubocop: - description: Run Rubocop - service: rails - command: bundle exec rubocop - compose_run_options: [no-deps] - - psql: - description: Run psql console - service: postgres - command: psql -h postgres -U postgres -d example_app_dev - - 'redis-cli': - description: Run Redis console - service: redis - command: redis-cli -h redis - -provision: - - dip compose down --volumes - - dip compose up -d postgres redis - - dip bundle install - - dip yarn install - - dip rails db:prepare - - dip rails db:test:prepare diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 561acfc9..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,185 +0,0 @@ -version: '2.4' - -x-app: &app - build: - context: . - dockerfile: ./.dockerdev/Dockerfile - args: - RUBY_VERSION: '3.0.1' - PG_MAJOR: '13' - NODE_MAJOR: '12' - YARN_VERSION: '1.13.0' - BUNDLER_VERSION: '2.2.15' - environment: &env - NODE_ENV: development - RAILS_ENV: ${RAILS_ENV:-development} - image: anycable-rails-demo-dev:1.3.0 - tmpfs: - - /tmp - -x-backend: &backend - <<: *app - stdin_open: true - tty: true - volumes: - - .:/app:cached - - rails_cache:/app/tmp/cache - - bundle:/usr/local/bundle - - node_modules:/app/node_modules - - packs:/app/public/packs - - packs-test:/app/public/packs-test - - .dockerdev/.psqlrc:/root/.psqlrc:ro - - .dockerdev/.bashrc:/root/.bashrc:ro - - .dockerdev/.pryrc:/root/.pryrc:ro - environment: &backend_environment - <<: *env - REDIS_URL: redis://redis:6379/ - DATABASE_URL: postgres://postgres:postgres@postgres:5432 - CHROME_URL: http://chrome:3333 - BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap - WEBPACKER_DEV_SERVER_HOST: webpacker - WEB_CONCURRENCY: 1 - HISTFILE: /app/log/.bash_history - PSQL_HISTFILE: /app/log/.psql_history - EDITOR: vi - LOG: ${LOG:-0} - depends_on: &backend_depends_on - postgres: - condition: service_healthy - redis: - condition: service_healthy - -services: - rails: - <<: *backend - command: bundle exec rails server -b 0.0.0.0 - ports: - - '3000:3000' - - # Separate definition for system tests to add Chrome as a dependency - rspec_system: - <<: *backend - ports: - - '3000:3000' - - '50051' - environment: - <<: *backend_environment - APP_HOST: anycable.test - ANYCABLE_RPC_HOST: 0.0.0.0:50051 - CABLE_URL: "ws://ws.anycable.test:8080/cable" - depends_on: - <<: *backend_depends_on - chrome: - condition: service_started - ws_test: - condition: service_started - networks: - default: - aliases: - - anycable.test - - sidekiq: - <<: *backend - command: bundle exec sidekiq -C config/sidekiq.yml - - ws: &ws - image: anycable/anycable-go:1.1.0.rc1 - ports: - - '8080:8080' - environment: &ws_environment - ANYCABLE_HOST: "0.0.0.0" - ANYCABLE_REDIS_URL: redis://redis:6379/0 - ANYCABLE_RPC_HOST: anycable:50051 - ANYCABLE_DEBUG: 1 - depends_on: - redis: - condition: service_healthy - - ws_test: - <<: *ws - ports: - - '8080' - environment: - <<: *ws_environment - ANYCABLE_RPC_HOST: anycable.test:50051 - networks: - default: - aliases: - - ws.anycable.test - - anycable: - <<: *backend - command: bundle exec anycable - environment: - <<: *backend_environment - ANYCABLE_REDIS_URL: redis://redis:6379/0 - ANYCABLE_RPC_HOST: 0.0.0.0:50051 - ANYCABLE_DEBUG: 1 - ports: - - '50051' - depends_on: - <<: *backend_depends_on - ws: - condition: service_started - - postgres: - image: postgres:13.0 - volumes: - - .dockerdev/.psqlrc:/root/.psqlrc:ro - - postgres:/var/lib/postgresql/data - - ./log:/root/log:cached - environment: - POSTGRES_PASSWORD: postgres - PSQL_HISTFILE: /root/log/.psql_history - ports: - - 5432 - healthcheck: - test: pg_isready -U postgres -h 127.0.0.1 - interval: 5s - - redis: - image: redis:5.0-alpine - volumes: - - redis:/data - ports: - - 6379 - healthcheck: - test: redis-cli ping - interval: 1s - timeout: 3s - retries: 30 - - webpacker: - <<: *app - command: bundle exec ./bin/webpack-dev-server - ports: - - '3035:3035' - - 3036 - volumes: - - .:/app:cached - - bundle:/usr/local/bundle - - node_modules:/app/node_modules - - packs:/app/public/packs - - packs-test:/app/public/packs-test - environment: - <<: *env - WEBPACKER_DEV_SERVER_HOST: 0.0.0.0 - - chrome: - image: browserless/chrome:1.31-chrome-stable - ports: - - '3333:3333' - environment: - PORT: 3333 - # Set connection timeout for debugging - # https://docs.browserless.io/docs/docker.html#connection-timeout - CONNECTION_TIMEOUT: 600000 - -volumes: - postgres: - redis: - bundle: - node_modules: - rails_cache: - packs: - packs-test: diff --git a/frontend/utils/cable.js b/frontend/utils/cable.js index 4c03011f..0a98cc7f 100644 --- a/frontend/utils/cable.js +++ b/frontend/utils/cable.js @@ -1,10 +1,13 @@ -import { createConsumer } from "@rails/actioncable"; +import { createConsumer, logger, adapters, INTERNAL } from "@rails/actioncable"; +import msgpack from "@ygoe/msgpack"; let consumer; export const createCable = () => { if (!consumer) { consumer = createConsumer(); + Object.assign(consumer.connection, connectionExtension); + Object.assign(consumer.connection.events, connectionEventsExtension); } return consumer; @@ -14,3 +17,68 @@ export const createChannel = (...args) => { const consumer = createCable(); return consumer.subscriptions.create(...args); }; + +// Msgpack support +// Patches this file: https://github.com/rails/rails/blob/main/actioncable/app/javascript/action_cable/connection.js + +// Replace JSON protocol with msgpack +const supportedProtocols = [ + "actioncable-v1-msgpack" +] + +const protocols = supportedProtocols +const { message_types } = INTERNAL + +const connectionExtension = { + open() { + if (this.isActive()) { + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) + return false + } else { + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) + if (this.webSocket) { this.uninstallEventHandlers() } + this.webSocket = new adapters.WebSocket(this.consumer.url, protocols) + this.webSocket.binaryType = "arraybuffer" + this.installEventHandlers() + this.monitor.start() + return true + } + }, + isProtocolSupported() { + return supportedProtocols[0] == this.getProtocol() + }, + send(data) { + if (this.isOpen()) { + const encoded = msgpack.encode(data); + this.webSocket.send(encoded) + return true + } else { + return false + } + } +} + +// Incoming messages are handled by the connection.events.message function. +// There is no way to patch it, so, we have to copy-paste :( +const connectionEventsExtension = { + message(event) { + if (!this.isProtocolSupported()) { return } + const {identifier, message, reason, reconnect, type} = msgpack.decode(new Uint8Array(event.data)) + switch (type) { + case message_types.welcome: + this.monitor.recordConnect() + return this.subscriptions.reload() + case message_types.disconnect: + logger.log(`Disconnecting. Reason: ${reason}`) + return this.close({allowReconnect: reconnect}) + case message_types.ping: + return this.monitor.recordPing() + case message_types.confirmation: + return this.subscriptions.notify(identifier, "connected") + case message_types.rejection: + return this.subscriptions.reject(identifier) + default: + return this.subscriptions.notify(identifier, "received", message) + } + }, +}; diff --git a/package.json b/package.json index 6c72aa29..b2542e82 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@rails/actioncable": "6.0.2", "@rails/ujs": "^6.0.3-4", "@rails/webpacker": "^6.0.0-beta.5", + "@ygoe/msgpack": "^1.0.2", "autoprefixer": "^10.2.5", "css-loader": "^5.1.3", "css-minimizer-webpack-plugin": "^1.3.0", diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 0d02bb0c..1668335f 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -5,8 +5,7 @@ module System def login_user(name) page.driver.set_cookie( :uid, - [name, Nanoid.generate(size: 3)].join("/"), - domain: CAPYBARA_COOKIE_DOMAIN + [name, Nanoid.generate(size: 3)].join("/") ) end diff --git a/spec/system/support/anycable_setup.rb b/spec/system/support/anycable_setup.rb index 9f5a7035..1168976d 100644 --- a/spec/system/support/anycable_setup.rb +++ b/spec/system/support/anycable_setup.rb @@ -4,17 +4,11 @@ # Run AnyCable RPC server RSpec.configure do |config| - # Skip assets precompilcation if we exclude system specs. + # Only start RPC server if system tests are included into the run next if config.filter.opposite.rules[:type] == "system" || config.exclude_pattern.match?(%r{spec/system}) require "anycable/cli" - - cli = AnyCable::CLI.new(embedded: true) - cli.run - - config.after(:suite) do - cli&.shutdown - end + AnyCable::CLI.embed! config.before(:each, type: :system) do next if ActionCable.server.pubsub.is_a?(ActionCable::SubscriptionAdapter::AnyCable) diff --git a/spec/system/support/better_rails_system_tests.rb b/spec/system/support/better_rails_system_tests.rb index 540dfad7..f25c7307 100644 --- a/spec/system/support/better_rails_system_tests.rb +++ b/spec/system/support/better_rails_system_tests.rb @@ -8,11 +8,6 @@ def absolute_image_path Rails.root.join("#{Capybara.save_path}/screenshots/#{image_name}.png") end - # Use relative path in screenshot message to make it clickable in VS Code when running in Docker - def image_path - absolute_image_path.relative_path_from(Rails.root).to_s - end - # Make failure screenshots compatible with multi-session setup def take_screenshot return super unless Capybara.last_used_session diff --git a/spec/system/support/capybara_setup.rb b/spec/system/support/capybara_setup.rb index 688bb5a1..6ec75298 100644 --- a/spec/system/support/capybara_setup.rb +++ b/spec/system/support/capybara_setup.rb @@ -2,21 +2,6 @@ # Capybara settings (not covered by Rails system tests) -# Make server listening on all hosts -Capybara.server_host = "0.0.0.0" -# Use a hostname accessible from the outside world -# NOTE: Rails overrides Capybara.app_host in Rails <6.1, so we have -# to store it differently -CAPYBARA_APP_HOST = "http://#{ENV.fetch("APP_HOST", `hostname`.strip&.downcase || "0.0.0.0")}" -Capybara.app_host = CAPYBARA_APP_HOST - -# Which domain to use when setting cookies directly in tests. -CAPYBARA_COOKIE_DOMAIN = URI.parse(CAPYBARA_APP_HOST).host.then do |host| - # If host is a top-level domain - next host unless host.include?(".") - ".#{host}" -end - # Don't wait too long in `have_xyz` matchers Capybara.default_max_wait_time = 2 @@ -26,6 +11,9 @@ # Where to store artifacts (e.g. screenshots, downloaded files, etc.) Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara") +# Use fixed server port to configure AnyCable broadcast url +Capybara.server_port = 3023 + Capybara.singleton_class.prepend(Module.new do attr_accessor :last_used_session diff --git a/spec/system/support/cuprite_setup.rb b/spec/system/support/cuprite_setup.rb index 1c964e65..d8062442 100644 --- a/spec/system/support/cuprite_setup.rb +++ b/spec/system/support/cuprite_setup.rb @@ -3,31 +3,6 @@ # Cuprite is a modern Capybara driver which uses Chrome CDP API # instead of Selenium & co. # See https://github.com/rubycdp/cuprite - -REMOTE_CHROME_URL = ENV["CHROME_URL"] -REMOTE_CHROME_HOST, REMOTE_CHROME_PORT = - if REMOTE_CHROME_URL - URI.parse(REMOTE_CHROME_URL).yield_self do |uri| - [uri.host, uri.port] - end - end - -# Check whether the remote chrome is running and configure the Capybara -# driver for it. -remote_chrome = - begin - if REMOTE_CHROME_URL.nil? - false - else - Socket.tcp(REMOTE_CHROME_HOST, REMOTE_CHROME_PORT, connect_timeout: 1).close - true - end - rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError - false - end - -remote_options = remote_chrome ? {url: REMOTE_CHROME_URL} : {} - require "capybara/cuprite" Capybara.register_driver(:cuprite) do |app| @@ -35,9 +10,14 @@ app, **{ window_size: [1200, 800], - browser_options: remote_chrome ? {"no-sandbox" => nil} : {}, - inspector: true - }.merge(remote_options) + browser_options: {}, + # Increase Chrome startup timeout for CI + process_timeout: 10, + inspector: true, + # Allow running Chrome in a headful mode by setting HEADLESS env + # var to a falsey value + headless: !ENV["HEADLESS"].in?(%w[n 0 no false]) + } ) end @@ -50,9 +30,7 @@ def pause end def debug(binding = nil) - $stdout.puts "🔎 Open Chrome inspector at http://localhost:3333" - return binding.pry if binding - page.driver.pause + page.driver.debug(binding) end end diff --git a/yarn.lock b/yarn.lock index f87bda1a..65b6bf5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1158,6 +1158,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@ygoe/msgpack@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@ygoe/msgpack/-/msgpack-1.0.2.tgz#7237b74179933bb9b73aa09d15d2a4414cfc7377" + integrity sha512-GXnUKo8aFiTE1RmhzGXzXNXoN500Zxe0FH62thZi18xI82N+Gcdw9MWjhV0SdTEfjHFwZiw0+ZJSE8sD02pRsQ== + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"