├── .circleci └── config.yml ├── .dockerdev ├── .bashrc ├── .pryrc └── Dockerfile ├── .gitignore ├── .rubocop.yml ├── Gemfile ├── Gemfile.lock ├── Makefile ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── application.js │ │ ├── baskets.coffee │ │ ├── channels │ │ │ ├── .keep │ │ │ ├── base.coffee │ │ │ ├── baskets.coffee │ │ │ ├── notification.coffee │ │ │ └── products.coffee │ │ ├── products.coffee │ │ ├── shared │ │ │ ├── app.coffee │ │ │ └── notifications.coffee │ │ └── templates │ │ │ ├── basket.jst.skim │ │ │ └── product.jst.skim │ └── stylesheets │ │ └── application.sass ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ ├── baskets_channel.rb │ ├── notification_channel.rb │ └── products_channel.rb ├── controllers │ ├── application_controller.rb │ ├── baskets_controller.rb │ ├── concerns │ │ ├── .keep │ │ └── serialized.rb │ ├── products_controller.rb │ └── sessions_controller.rb ├── forms │ └── session_form.rb ├── helpers │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── models │ ├── application_record.rb │ ├── basket.rb │ ├── concerns │ │ └── .keep │ └── product.rb ├── serializers │ ├── basket_serializer.rb │ └── product_serializer.rb └── views │ ├── baskets │ ├── _basket.slim │ ├── _form.slim │ ├── index.slim │ └── show.slim │ ├── layouts │ └── application.html.slim │ ├── products │ ├── _form.slim │ └── _product.slim │ ├── sessions │ └── new.slim │ └── shared │ └── _add_btn.slim ├── bin ├── bundle ├── cable ├── rackup ├── rails ├── rake ├── rspec ├── setup ├── spring ├── update └── yarn ├── cable └── config.ru ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── active_model_serializers.rb │ ├── active_record_belongs_to_required_by_default.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── cors.rb │ ├── faker.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── per_form_csrf_tokens.rb │ ├── request_forgery_protection.rb │ ├── session_store.rb │ ├── ssl_options.rb │ ├── to_time_preserves_timezone.rb │ ├── wrap_parameters.rb │ └── yabeda.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20160620113623_create_baskets.rb │ └── 20160621123902_create_products.rb ├── schema.rb └── seeds.rb ├── dip.yml ├── docker-compose.yml ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ ├── heroku.rake │ └── rpc.rake ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── rebar.lock ├── spec ├── acceptance │ ├── create_basket_spec.rb │ ├── create_product_spec.rb │ ├── disconnection_spec.rb │ └── notifications_spec.rb ├── acceptance_helper.rb ├── bg_helper.rb ├── factories │ └── baskets.rb ├── rails_helper.rb ├── shared_contexts │ ├── shared_bg.rb │ └── shared_feature.rb ├── spec_helper.rb └── support │ └── acceptance_helper.rb └── vendor └── assets ├── javascripts ├── .keep └── phantom.js └── stylesheets └── .keep /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | version: 2 5 | test: 6 | jobs: 7 | - checkout_code 8 | - bundle_install: 9 | requires: 10 | - checkout_code 11 | # - rspec: 12 | # requires: 13 | # - bundle_install 14 | - rubocop: 15 | requires: 16 | - bundle_install 17 | 18 | executors: 19 | ruby: 20 | docker: 21 | - image: circleci/ruby:2.5.7-node-browsers 22 | environment: 23 | BUNDLE_PATH: vendor/bundle 24 | GEM_HOME: vendor/bundle 25 | RAILS_ENV: test 26 | 27 | jobs: 28 | checkout_code: 29 | executor: ruby 30 | steps: 31 | - attach_workspace: 32 | at: . 33 | - restore_cache: 34 | keys: 35 | - anycable_demo-source-v1-{{ .Branch }}-{{ .Revision }} 36 | - anycable_demo-source-v1-{{ .Branch }} 37 | - anycable_demo-source-v1 38 | - checkout 39 | - save_cache: 40 | key: anycable_demo-source-v1-{{ .Branch }}-{{ .Revision }} 41 | paths: 42 | - ".git" 43 | - run: 44 | name: "Drop git directory to save some space" 45 | command: "rm -rf .git/" 46 | - persist_to_workspace: 47 | root: . 48 | paths: . 49 | 50 | bundle_install: 51 | executor: ruby 52 | environment: 53 | BUNDLE_JOBS: 3 54 | BUNDLE_RETRY: 3 55 | steps: 56 | - attach_workspace: 57 | at: . 58 | - restore_cache: 59 | keys: 60 | - anycable_demo-bundle-2.5-{{ checksum "Gemfile.lock" }} 61 | - anycable_demo-bundle-2.5 62 | - run: 63 | name: Install bundler version from Gemfile 64 | command: | 65 | export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ") 66 | gem install bundler:$BUNDLER_VERSION --no-document --conservative --minimal-deps 67 | - run: 68 | name: Which bundler? 69 | command: bundle -v 70 | - run: 71 | name: Bundle Install 72 | command: bundle check || bundle install 73 | - run: 74 | name: Clean stale gems 75 | command: | 76 | bundle clean --force 77 | rm -rf $BUNDLE_PATH/cache/* 78 | - save_cache: 79 | key: anycable_demo-bundle-2.5-{{ checksum "Gemfile.lock" }} 80 | paths: vendor/bundle 81 | - persist_to_workspace: 82 | root: . 83 | paths: vendor/bundle 84 | 85 | rspec: 86 | executor: ruby 87 | steps: 88 | - attach_workspace: 89 | at: . 90 | 91 | - run: 92 | name: Install hivemind 93 | command: | 94 | mkdir -p $HOME/.local/bin/ 95 | curl -sSL https://github.com/DarthSim/hivemind/releases/download/v1.0.6/hivemind-v1.0.6-linux-amd64.gz -o $HOME/.local/bin/hivemind.gz 96 | gunzip $HOME/.local/bin/hivemind.gz 97 | chmod +x $HOME/.local/bin/hivemind 98 | 99 | - run: 100 | name: Run specs 101 | command: | 102 | bundle exec rspec \ 103 | --format RspecJunitFormatter \ 104 | --out /tmp/test-results/rspec.xml \ 105 | --format progress 106 | 107 | - store_test_results: 108 | path: /tmp/test-results 109 | 110 | rubocop: 111 | executor: ruby 112 | steps: 113 | - attach_workspace: 114 | at: . 115 | - run: 116 | command: | 117 | bundle exec rubocop 118 | -------------------------------------------------------------------------------- /.dockerdev/.bashrc: -------------------------------------------------------------------------------- 1 | alias be="bundle exec" 2 | -------------------------------------------------------------------------------- /.dockerdev/.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV["HISTFILE"] 4 | hist_dir = ENV["HISTFILE"].sub(/\/[^\/]+$/, "") 5 | Pry.config.history_save = true 6 | Pry.config.history_file = File.join(hist_dir, ".pry_history") 7 | end 8 | -------------------------------------------------------------------------------- /.dockerdev/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION 2 | FROM ruby:${RUBY_VERSION} 3 | 4 | ARG NODE_MAJOR 5 | ARG BUNDLER_VERSION 6 | 7 | # Add NodeJS and Yarn to the sources list, install application dependecies 8 | RUN \ 9 | curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - && \ 10 | apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \ 11 | DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ 12 | nodejs \ 13 | build-essential \ 14 | gnupg2 \ 15 | curl \ 16 | less \ 17 | git \ 18 | locales \ 19 | tzdata \ 20 | time \ 21 | && update-locale LANG=C.UTF-8 LC_ALL=C.UTF-8 \ 22 | && apt-get autoremove -y \ 23 | && apt-get clean \ 24 | && rm -rf /var/cache/apt/archives/* \ 25 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ 26 | && truncate -s 0 /var/log/*log 27 | 28 | ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 29 | 30 | # Upgrade RubyGems and install required Bundler version 31 | RUN gem update --system && \ 32 | gem install bundler:$BUNDLER_VERSION 33 | 34 | # Create a directory for the app code 35 | RUN mkdir -p /app 36 | 37 | WORKDIR /app 38 | 39 | CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | /vendor/bundle 10 | /coverage 11 | .rspec 12 | /.capistrano 13 | # Ignore the default SQLite database. 14 | 15 | # production files 16 | /tmp 17 | 18 | *.log 19 | *.swp 20 | 21 | # ignore assets 22 | /public/assets 23 | /public/system 24 | /public/system_test* 25 | .DS_Store 26 | 27 | *.sqlite3 28 | 29 | /.idea 30 | 31 | .vagrant 32 | .sass-cache 33 | 34 | *.retry 35 | *.pid -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | # Include gemspec and Rakefile 3 | Include: 4 | - 'lib/**/*.rb' 5 | - 'lib/**/*.rake' 6 | - 'spec/**/*.rb' 7 | Exclude: 8 | - 'bin/**/*' 9 | - 'spec/dummy/**/*' 10 | - 'tmp/**/*' 11 | - 'bench/**/*' 12 | - 'vendor/**/*' 13 | DisplayCopNames: true 14 | StyleGuideCopsOnly: false 15 | TargetRubyVersion: 2.5 16 | 17 | Naming/AccessorMethodName: 18 | Enabled: false 19 | 20 | Style/TrivialAccessors: 21 | Enabled: false 22 | 23 | Style/Documentation: 24 | Exclude: 25 | - 'spec/**/*.rb' 26 | 27 | Style/StringLiterals: 28 | Enabled: false 29 | 30 | Layout/SpaceInsideStringInterpolation: 31 | EnforcedStyle: no_space 32 | 33 | Style/BlockDelimiters: 34 | Exclude: 35 | - 'spec/**/*.rb' 36 | 37 | Lint/AmbiguousRegexpLiteral: 38 | Enabled: false 39 | 40 | Metrics/MethodLength: 41 | Exclude: 42 | - 'spec/**/*.rb' 43 | 44 | Metrics/LineLength: 45 | Max: 100 46 | Exclude: 47 | - 'spec/**/*.rb' 48 | 49 | Metrics/BlockLength: 50 | Exclude: 51 | - 'spec/**/*.rb' 52 | 53 | Rails/Date: 54 | Enabled: false 55 | 56 | Rails/TimeZone: 57 | Enabled: false -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby "~> 2.6" 4 | 5 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 6 | gem 'rails', '~> 6.0' 7 | # Use sqlite3 8 | gem 'sqlite3', '~> 1.4', group: [:development, :test] 9 | gem 'pg', group: :production 10 | 11 | # Use Puma as the app server 12 | gem 'puma', '~> 4.0' 13 | 14 | gem 'sass-rails' 15 | gem 'uglifier', '>= 1.3.0' 16 | gem 'coffee-rails' 17 | gem 'slim-rails' 18 | gem 'autoprefixer-rails' 19 | gem 'csso-rails' 20 | gem 'jquery-rails' 21 | gem 'materialize-sass', '~> 0.100' 22 | gem 'material_icons' 23 | gem 'skim' 24 | gem 'gon' 25 | 26 | gem 'redis', '~> 4.0' 27 | 28 | gem 'rack-cors' 29 | 30 | # Other 31 | gem 'nenv' 32 | 33 | if ENV["LOCAL_CABLE"] 34 | gem 'anycable', path: '../anycable' 35 | gem 'anycable-rails', path: '../anycable-rails' 36 | else 37 | gem 'anycable-rails', '1.0.0.preview2' 38 | end 39 | 40 | # Not support by 1.0.0 yet 41 | # gem 'anycable-rack-server', require: ENV["ANYCABLE_RACK"] ? "anycable-rack-server" : false 42 | 43 | gem 'yabeda' 44 | 45 | gem 'prometheus-client' 46 | gem 'yabeda-prometheus' 47 | 48 | gem 'tzinfo' 49 | gem 'tzinfo-data' 50 | 51 | gem 'active_model_serializers' 52 | 53 | gem 'factory_bot_rails', '~> 4.0' 54 | 55 | gem "faker", "~> 1.8.4" 56 | 57 | group :development, :test do 58 | gem 'pry' 59 | gem 'pry-byebug' 60 | gem 'pry-rails' 61 | end 62 | 63 | group :development do 64 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 65 | gem 'spring' 66 | gem 'listen', '~> 3.0.5' 67 | gem 'spring-watcher-listen', '~> 2.0.0' 68 | 69 | gem 'better_errors' 70 | gem 'binding_of_caller' 71 | 72 | # Code audit 73 | gem 'rubocop', require: false 74 | end 75 | 76 | group :test do 77 | # RSpec tools 78 | gem 'rspec-rails', '~> 3.9.0' 79 | gem "rspec_junit_formatter" # For CircleCI reports 80 | gem 'capybara' 81 | gem 'fuubar' 82 | gem 'database_cleaner' 83 | gem 'shoulda-matchers' 84 | gem 'timecop' 85 | gem 'zonebie' 86 | gem 'json_spec' 87 | gem 'show_me_the_cookies' 88 | 89 | gem 'chromedriver-helper' 90 | gem 'selenium-webdriver', require: false 91 | 92 | gem 'rack_session_access' 93 | end 94 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (6.0.2.2) 5 | actionpack (= 6.0.2.2) 6 | nio4r (~> 2.0) 7 | websocket-driver (>= 0.6.1) 8 | actionmailbox (6.0.2.2) 9 | actionpack (= 6.0.2.2) 10 | activejob (= 6.0.2.2) 11 | activerecord (= 6.0.2.2) 12 | activestorage (= 6.0.2.2) 13 | activesupport (= 6.0.2.2) 14 | mail (>= 2.7.1) 15 | actionmailer (6.0.2.2) 16 | actionpack (= 6.0.2.2) 17 | actionview (= 6.0.2.2) 18 | activejob (= 6.0.2.2) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (6.0.2.2) 22 | actionview (= 6.0.2.2) 23 | activesupport (= 6.0.2.2) 24 | rack (~> 2.0, >= 2.0.8) 25 | rack-test (>= 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 28 | actiontext (6.0.2.2) 29 | actionpack (= 6.0.2.2) 30 | activerecord (= 6.0.2.2) 31 | activestorage (= 6.0.2.2) 32 | activesupport (= 6.0.2.2) 33 | nokogiri (>= 1.8.5) 34 | actionview (6.0.2.2) 35 | activesupport (= 6.0.2.2) 36 | builder (~> 3.1) 37 | erubi (~> 1.4) 38 | rails-dom-testing (~> 2.0) 39 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 40 | active_model_serializers (0.10.10) 41 | actionpack (>= 4.1, < 6.1) 42 | activemodel (>= 4.1, < 6.1) 43 | case_transform (>= 0.2) 44 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3) 45 | activejob (6.0.2.2) 46 | activesupport (= 6.0.2.2) 47 | globalid (>= 0.3.6) 48 | activemodel (6.0.2.2) 49 | activesupport (= 6.0.2.2) 50 | activerecord (6.0.2.2) 51 | activemodel (= 6.0.2.2) 52 | activesupport (= 6.0.2.2) 53 | activestorage (6.0.2.2) 54 | actionpack (= 6.0.2.2) 55 | activejob (= 6.0.2.2) 56 | activerecord (= 6.0.2.2) 57 | marcel (~> 0.3.1) 58 | activesupport (6.0.2.2) 59 | concurrent-ruby (~> 1.0, >= 1.0.2) 60 | i18n (>= 0.7, < 2) 61 | minitest (~> 5.1) 62 | tzinfo (~> 1.1) 63 | zeitwerk (~> 2.2) 64 | addressable (2.7.0) 65 | public_suffix (>= 2.0.2, < 5.0) 66 | anycable (1.0.0.preview2) 67 | anyway_config (>= 1.4.2) 68 | grpc (~> 1.17) 69 | anycable-rails (1.0.0.preview2) 70 | anycable (~> 1.0.0.preview2) 71 | rails (>= 5) 72 | anyway_config (2.0.2) 73 | ruby-next-core (>= 0.5.1) 74 | archive-zip (0.12.0) 75 | io-like (~> 0.3.0) 76 | ast (2.4.0) 77 | autoprefixer-rails (9.7.6) 78 | execjs 79 | better_errors (2.7.0) 80 | coderay (>= 1.0.0) 81 | erubi (>= 1.0.0) 82 | rack (>= 0.9.0) 83 | binding_of_caller (0.8.0) 84 | debug_inspector (>= 0.0.1) 85 | builder (3.2.4) 86 | byebug (11.1.3) 87 | capybara (3.32.1) 88 | addressable 89 | mini_mime (>= 0.1.3) 90 | nokogiri (~> 1.8) 91 | rack (>= 1.6.0) 92 | rack-test (>= 0.6.3) 93 | regexp_parser (~> 1.5) 94 | xpath (~> 3.2) 95 | case_transform (0.2) 96 | activesupport 97 | childprocess (3.0.0) 98 | chromedriver-helper (2.1.1) 99 | archive-zip (~> 0.10) 100 | nokogiri (~> 1.8) 101 | coderay (1.1.2) 102 | coffee-rails (5.0.0) 103 | coffee-script (>= 2.2.0) 104 | railties (>= 5.2.0) 105 | coffee-script (2.4.1) 106 | coffee-script-source 107 | execjs 108 | coffee-script-source (1.12.2) 109 | concurrent-ruby (1.1.6) 110 | crass (1.0.6) 111 | csso-rails (0.8.2) 112 | execjs (>= 1) 113 | database_cleaner (1.8.4) 114 | debug_inspector (0.0.3) 115 | diff-lcs (1.3) 116 | dry-initializer (3.0.3) 117 | erubi (1.9.0) 118 | execjs (2.7.0) 119 | factory_bot (4.11.1) 120 | activesupport (>= 3.0.0) 121 | factory_bot_rails (4.11.1) 122 | factory_bot (~> 4.11.1) 123 | railties (>= 3.0.0) 124 | faker (1.8.7) 125 | i18n (>= 0.7) 126 | ffi (1.12.2) 127 | fuubar (2.5.0) 128 | rspec-core (~> 3.0) 129 | ruby-progressbar (~> 1.4) 130 | globalid (0.4.2) 131 | activesupport (>= 4.2.0) 132 | gon (6.3.2) 133 | actionpack (>= 3.0.20) 134 | i18n (>= 0.7) 135 | multi_json 136 | request_store (>= 1.0) 137 | google-protobuf (3.11.4) 138 | googleapis-common-protos-types (1.0.5) 139 | google-protobuf (~> 3.11) 140 | grpc (1.28.0) 141 | google-protobuf (~> 3.11) 142 | googleapis-common-protos-types (~> 1.0) 143 | i18n (1.8.2) 144 | concurrent-ruby (~> 1.0) 145 | io-like (0.3.1) 146 | jaro_winkler (1.5.4) 147 | jquery-rails (4.3.5) 148 | rails-dom-testing (>= 1, < 3) 149 | railties (>= 4.2.0) 150 | thor (>= 0.14, < 2.0) 151 | json_spec (1.1.5) 152 | multi_json (~> 1.0) 153 | rspec (>= 2.0, < 4.0) 154 | jsonapi-renderer (0.2.2) 155 | listen (3.0.8) 156 | rb-fsevent (~> 0.9, >= 0.9.4) 157 | rb-inotify (~> 0.9, >= 0.9.7) 158 | loofah (2.5.0) 159 | crass (~> 1.0.2) 160 | nokogiri (>= 1.5.9) 161 | mail (2.7.1) 162 | mini_mime (>= 0.1.1) 163 | marcel (0.3.3) 164 | mimemagic (~> 0.3.2) 165 | material_icons (2.2.1) 166 | railties (>= 3.2) 167 | materialize-sass (0.100.2.1) 168 | sass (~> 3.3) 169 | method_source (1.0.0) 170 | mimemagic (0.3.4) 171 | mini_mime (1.0.2) 172 | mini_portile2 (2.4.0) 173 | minitest (5.14.0) 174 | multi_json (1.14.1) 175 | nenv (0.3.0) 176 | nio4r (2.5.2) 177 | nokogiri (1.10.9) 178 | mini_portile2 (~> 2.4.0) 179 | parallel (1.19.1) 180 | parser (2.7.1.1) 181 | ast (~> 2.4.0) 182 | pg (1.2.3) 183 | prometheus-client (2.0.0) 184 | pry (0.13.1) 185 | coderay (~> 1.1) 186 | method_source (~> 1.0) 187 | pry-byebug (3.9.0) 188 | byebug (~> 11.0) 189 | pry (~> 0.13.0) 190 | pry-rails (0.3.9) 191 | pry (>= 0.10.4) 192 | public_suffix (4.0.4) 193 | puma (4.3.3) 194 | nio4r (~> 2.0) 195 | rack (2.2.2) 196 | rack-cors (1.1.1) 197 | rack (>= 2.0.0) 198 | rack-test (1.1.0) 199 | rack (>= 1.0, < 3) 200 | rack_session_access (0.2.0) 201 | builder (>= 2.0.0) 202 | rack (>= 1.0.0) 203 | rails (6.0.2.2) 204 | actioncable (= 6.0.2.2) 205 | actionmailbox (= 6.0.2.2) 206 | actionmailer (= 6.0.2.2) 207 | actionpack (= 6.0.2.2) 208 | actiontext (= 6.0.2.2) 209 | actionview (= 6.0.2.2) 210 | activejob (= 6.0.2.2) 211 | activemodel (= 6.0.2.2) 212 | activerecord (= 6.0.2.2) 213 | activestorage (= 6.0.2.2) 214 | activesupport (= 6.0.2.2) 215 | bundler (>= 1.3.0) 216 | railties (= 6.0.2.2) 217 | sprockets-rails (>= 2.0.0) 218 | rails-dom-testing (2.0.3) 219 | activesupport (>= 4.2.0) 220 | nokogiri (>= 1.6) 221 | rails-html-sanitizer (1.3.0) 222 | loofah (~> 2.3) 223 | railties (6.0.2.2) 224 | actionpack (= 6.0.2.2) 225 | activesupport (= 6.0.2.2) 226 | method_source 227 | rake (>= 0.8.7) 228 | thor (>= 0.20.3, < 2.0) 229 | rainbow (3.0.0) 230 | rake (13.0.1) 231 | rb-fsevent (0.10.3) 232 | rb-inotify (0.10.1) 233 | ffi (~> 1.0) 234 | redis (4.1.3) 235 | regexp_parser (1.7.0) 236 | request_store (1.5.0) 237 | rack (>= 1.4) 238 | rexml (3.2.4) 239 | rspec (3.9.0) 240 | rspec-core (~> 3.9.0) 241 | rspec-expectations (~> 3.9.0) 242 | rspec-mocks (~> 3.9.0) 243 | rspec-core (3.9.1) 244 | rspec-support (~> 3.9.1) 245 | rspec-expectations (3.9.1) 246 | diff-lcs (>= 1.2.0, < 2.0) 247 | rspec-support (~> 3.9.0) 248 | rspec-mocks (3.9.1) 249 | diff-lcs (>= 1.2.0, < 2.0) 250 | rspec-support (~> 3.9.0) 251 | rspec-rails (3.9.1) 252 | actionpack (>= 3.0) 253 | activesupport (>= 3.0) 254 | railties (>= 3.0) 255 | rspec-core (~> 3.9.0) 256 | rspec-expectations (~> 3.9.0) 257 | rspec-mocks (~> 3.9.0) 258 | rspec-support (~> 3.9.0) 259 | rspec-support (3.9.2) 260 | rspec_junit_formatter (0.4.1) 261 | rspec-core (>= 2, < 4, != 2.12.0) 262 | rubocop (0.82.0) 263 | jaro_winkler (~> 1.5.1) 264 | parallel (~> 1.10) 265 | parser (>= 2.7.0.1) 266 | rainbow (>= 2.2.2, < 4.0) 267 | rexml 268 | ruby-progressbar (~> 1.7) 269 | unicode-display_width (>= 1.4.0, < 2.0) 270 | ruby-next-core (0.6.0) 271 | ruby-progressbar (1.10.1) 272 | rubyzip (2.3.0) 273 | sass (3.7.4) 274 | sass-listen (~> 4.0.0) 275 | sass-listen (4.0.0) 276 | rb-fsevent (~> 0.9, >= 0.9.4) 277 | rb-inotify (~> 0.9, >= 0.9.7) 278 | sass-rails (6.0.0) 279 | sassc-rails (~> 2.1, >= 2.1.1) 280 | sassc (2.3.0) 281 | ffi (~> 1.9) 282 | sassc-rails (2.1.2) 283 | railties (>= 4.0.0) 284 | sassc (>= 2.0) 285 | sprockets (> 3.0) 286 | sprockets-rails 287 | tilt 288 | selenium-webdriver (3.142.7) 289 | childprocess (>= 0.5, < 4.0) 290 | rubyzip (>= 1.2.2) 291 | shoulda-matchers (4.3.0) 292 | activesupport (>= 4.2.0) 293 | show_me_the_cookies (5.0.0) 294 | capybara (>= 2, < 4) 295 | skim (0.10.0) 296 | coffee-script 297 | coffee-script-source (>= 1.2.0) 298 | slim (>= 3.0) 299 | sprockets (>= 2, < 4) 300 | slim (4.0.1) 301 | temple (>= 0.7.6, < 0.9) 302 | tilt (>= 2.0.6, < 2.1) 303 | slim-rails (3.2.0) 304 | actionpack (>= 3.1) 305 | railties (>= 3.1) 306 | slim (>= 3.0, < 5.0) 307 | spring (2.1.0) 308 | spring-watcher-listen (2.0.1) 309 | listen (>= 2.7, < 4.0) 310 | spring (>= 1.2, < 3.0) 311 | sprockets (3.7.2) 312 | concurrent-ruby (~> 1.0) 313 | rack (> 1, < 3) 314 | sprockets-rails (3.2.1) 315 | actionpack (>= 4.0) 316 | activesupport (>= 4.0) 317 | sprockets (>= 3.0.0) 318 | sqlite3 (1.4.2) 319 | temple (0.8.2) 320 | thor (1.0.1) 321 | thread_safe (0.3.6) 322 | tilt (2.0.10) 323 | timecop (0.9.1) 324 | tzinfo (1.2.7) 325 | thread_safe (~> 0.1) 326 | tzinfo-data (1.2020.1) 327 | tzinfo (>= 1.0.0) 328 | uglifier (4.2.0) 329 | execjs (>= 0.3.0, < 3) 330 | unicode-display_width (1.7.0) 331 | websocket-driver (0.7.1) 332 | websocket-extensions (>= 0.1.0) 333 | websocket-extensions (0.1.4) 334 | xpath (3.2.0) 335 | nokogiri (~> 1.8) 336 | yabeda (0.5.0) 337 | concurrent-ruby 338 | dry-initializer 339 | yabeda-prometheus (0.6.0) 340 | prometheus-client (>= 0.10, < 3.0) 341 | yabeda (~> 0.5) 342 | zeitwerk (2.3.0) 343 | zonebie (0.6.1) 344 | 345 | PLATFORMS 346 | ruby 347 | 348 | DEPENDENCIES 349 | active_model_serializers 350 | anycable-rails (= 1.0.0.preview2) 351 | autoprefixer-rails 352 | better_errors 353 | binding_of_caller 354 | capybara 355 | chromedriver-helper 356 | coffee-rails 357 | csso-rails 358 | database_cleaner 359 | factory_bot_rails (~> 4.0) 360 | faker (~> 1.8.4) 361 | fuubar 362 | gon 363 | jquery-rails 364 | json_spec 365 | listen (~> 3.0.5) 366 | material_icons 367 | materialize-sass (~> 0.100) 368 | nenv 369 | pg 370 | prometheus-client 371 | pry 372 | pry-byebug 373 | pry-rails 374 | puma (~> 4.0) 375 | rack-cors 376 | rack_session_access 377 | rails (~> 6.0) 378 | redis (~> 4.0) 379 | rspec-rails (~> 3.9.0) 380 | rspec_junit_formatter 381 | rubocop 382 | sass-rails 383 | selenium-webdriver 384 | shoulda-matchers 385 | show_me_the_cookies 386 | skim 387 | slim-rails 388 | spring 389 | spring-watcher-listen (~> 2.0.0) 390 | sqlite3 (~> 1.4) 391 | timecop 392 | tzinfo 393 | tzinfo-data 394 | uglifier (>= 1.3.0) 395 | yabeda 396 | yabeda-prometheus 397 | zonebie 398 | 399 | RUBY VERSION 400 | ruby 2.6.5p114 401 | 402 | BUNDLED WITH 403 | 2.1.2 404 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ADAPTER=any_cable BG_WAIT='Handle WebSocket connections at /cable' CABLE_URL='ws://localhost:3334/cable' bundle exec rspec 3 | 4 | test-erl: 5 | ADAPTER=any_cable PROCFILE='Procfile.spec_erl' BG_WAIT='Booted erlycable' CABLE_URL='ws://localhost:3335/ws/cable' bundle exec rspec 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: [[ "$ANYCABLE_DEPLOYMENT" == "true" ]] && bundle exec anycable --server-command="anycable-go" || bundle exec rails server -p $PORT -b 0.0.0.0 2 | release: bundle exec rake heroku:release 3 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: ADAPTER=any_cable CABLE_URL='ws://localhost:3334/cable' bundle exec rails s -b 0.0.0.0 2 | rpc: ADAPTER=any_cable bundle exec anycable --server-command="anycable-go --port 3334" 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnyCable Demo 2 | 3 | **NOTE:** This demo application is no longer maintained. Please, refer to a new [AnyCable Rails Demo](https://github.com/anycable/anycable_rails_demo). 4 | 5 | ## Docker development environment 6 | 7 | ### Requirements 8 | 9 | - `docker` and `docker-compose` installed. 10 | 11 | For MacOS just use [official app](https://docs.docker.com/engine/installation/mac/). 12 | 13 | - [`dip`](https://github.com/bibendi/dip) installed. 14 | 15 | You can install `dip` either as Ruby gem: 16 | 17 | ```sh 18 | gem install dip 19 | ``` 20 | 21 | Or using Homebrew: 22 | 23 | ```sh 24 | brew tap bibendi/dip 25 | brew install dip 26 | ``` 27 | 28 | Or by downloading a binary (see [releases](https://github.com/bibendi/dip/releases)): 29 | 30 | ```sh 31 | curl -L https://github.com/bibendi/dip/releases/download/v5.0.0/dip-`uname -s`-`uname -m` > /usr/local/bin/dip 32 | chmod +x /usr/local/bin/dip 33 | ``` 34 | 35 | ### Usage 36 | 37 | First, run the following command to build images and provision the application: 38 | 39 | ```sh 40 | dip provision 41 | ``` 42 | 43 | Then, you can start Rails server alongside with AnyCable RPC and WebSocket server by running: 44 | 45 | ```sh 46 | dip up web 47 | ``` 48 | 49 | Then go to [http://localhost:3000/](http://localhost:3000/) and see the application in action. 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require action_cable 16 | //= require materialize-sprockets 17 | //= require_tree ./shared 18 | //= require_tree ./channels 19 | //= require_tree ./templates 20 | 21 | $(document).ready(function() { 22 | $('select').material_select(); 23 | }); 24 | 25 | window.after = function(time, fn) { 26 | return setTimeout(fn, time); 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/javascripts/baskets.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | basketForm = $("#basket_form") 3 | addBusketBtn = $("#add_basket_btn") 4 | basketsList = $("#baskets_list") 5 | 6 | basketsChannel = App.cable.subscriptions.create 'BasketsChannel', BasketsChannel 7 | 8 | basketsChannel.handle_message = (type, data) -> 9 | switch type 10 | when 'destroy' then $("#basket_#{data.id}").remove() 11 | when 'create' then addBasket(data) 12 | when 'products-update' then updateProductCount(data) 13 | 14 | addBasket = (data) -> 15 | return if $("#basket_#{data.id}")[0] 16 | basketsList.empty() unless basketsList.find('.basket').length 17 | basketsList.append App.utils.render('basket', data) 18 | 19 | updateProductCount = (data) -> 20 | basket = basketsList.find("#basket_#{data.basket_id}") 21 | return unless basket 22 | badge = basket.find('.badge') 23 | badge.text data.count 24 | 25 | addBusketBtn.on 'click', (e) -> 26 | e.preventDefault() 27 | basketForm.show() 28 | basketForm.find(".cancel-btn").one 'click', -> 29 | basketForm.hide() 30 | false 31 | 32 | basketForm.on 'ajax:success', (e, data, status, xhr) -> 33 | App.utils.successMessage(data?.message) 34 | addBasket(data.basket) 35 | basketForm.hide() 36 | 37 | basketForm.on 'ajax:error', App.utils.ajaxErrorHandler 38 | 39 | basketsList.on 'ajax:success', '.delete-basket-link', (e, data) -> 40 | App.utils.successMessage(data?.message) 41 | $(e.target).closest('.basket')?.remove() 42 | 43 | basketsList.on 'ajax:error', '.delete-basket-link', App.utils.ajaxErrorHandler 44 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/channels/base.coffee: -------------------------------------------------------------------------------- 1 | window.BaseChannel = { 2 | log: (msg) -> 3 | App.utils.successMessage("[ActionCable##{@name()}] #{msg}") 4 | 5 | connected: -> 6 | @log 'Connected' 7 | after 100, => @handle_connected?() 8 | 9 | disconnected: -> 10 | @log 'Disconnected' 11 | 12 | received: (data) -> 13 | @log 'Message Received' 14 | @handle_message?(data.type, data.data) 15 | } -------------------------------------------------------------------------------- /app/assets/javascripts/channels/baskets.coffee: -------------------------------------------------------------------------------- 1 | #= require ./base 2 | 3 | window.BasketsChannel = Object.assign({ 4 | handle_connected: -> @perform 'follow', 5 | name: -> 'BasketsChannel' 6 | }, BaseChannel) 7 | 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/notification.coffee: -------------------------------------------------------------------------------- 1 | #= require ./base 2 | 3 | window.NotificationChannel = Object.assign({ 4 | handle_connected: -> @perform 'follow' 5 | name: -> 'NotificationChannel' 6 | }, BaseChannel) 7 | 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/products.coffee: -------------------------------------------------------------------------------- 1 | #= require ./base 2 | 3 | window.ProductsChannel = Object.assign({ 4 | handle_connected: -> 5 | @perform 'follow', id: gon.basket_id 6 | name: -> 'ProductsChannel' 7 | }, BaseChannel) 8 | 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/products.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | productForm = $("#product_form") 3 | addBusketBtn = $("#add_product_btn") 4 | productsList = $("#products_list") 5 | 6 | productsChannel = App.cable.subscriptions.create 'ProductsChannel', ProductsChannel 7 | 8 | productsChannel.handle_message = (type, data) -> 9 | if type is 'destroy' 10 | $("#product_#{data.id}").remove() 11 | else if type is 'create' 12 | addProduct(data) 13 | 14 | addProduct = (data) -> 15 | return if $("#product_#{data.id}")[0] 16 | productsList.empty() unless productsList.find('.product-content').length 17 | productsList.append App.utils.render('product', data) 18 | 19 | addBusketBtn.on 'click', (e) -> 20 | e.preventDefault() 21 | productForm.show() 22 | productForm.find(".cancel-btn").one 'click', -> 23 | productForm.hide() 24 | false 25 | 26 | productForm.on 'ajax:success', (e, data, status, xhr) -> 27 | App.utils.successMessage(data?.message) 28 | addProduct(data.product) 29 | productForm.hide() 30 | 31 | productForm.on 'ajax:error', App.utils.ajaxErrorHandler 32 | 33 | productsList.on 'ajax:success', '.delete-product-link', (e, data) -> 34 | App.utils.successMessage(data?.message) 35 | $(e.target).closest('.collection-item')?.remove() 36 | 37 | productsList.on 'ajax:error', '.delete-product-link', App.utils.ajaxErrorHandler -------------------------------------------------------------------------------- /app/assets/javascripts/shared/app.coffee: -------------------------------------------------------------------------------- 1 | App = window.App = {} 2 | 3 | connected = true 4 | 5 | App.connect = -> 6 | return if connected 7 | App.cable.connection.monitor.reconnectAttempts = 2 8 | App.cable.connection.monitor.start() 9 | connected = true 10 | 11 | App.disconnect = -> 12 | return unless connected 13 | App.cable.connection.monitor.stop() 14 | # to make sure that it won't try to reconnect 15 | App.cable.connection.monitor.reconnectAttempts = 2 16 | App.cable.connection.close() 17 | connected = false 18 | 19 | App.utils = 20 | successMessage: (message) -> 21 | return unless message 22 | console.info(message) 23 | Materialize.toast(message, 2000, 'green') 24 | 25 | errorMessage: (message) -> 26 | return unless message 27 | console.warn(message) 28 | Materialize.toast(message, 4000, 'red') 29 | 30 | simpleMessage: (message) -> 31 | return unless message 32 | console.log(message) 33 | Materialize.toast(message, 4000, 'grey') 34 | 35 | ajaxErrorHandler: (e, data) -> 36 | message = 'Unknown error' 37 | if data.status == 401 38 | message = 'Sign in, please' 39 | else if data.status == 404 40 | message = 'Not found' 41 | else if data.status >= 400 && data.status < 500 42 | message = data.responseText 43 | 44 | App.utils.errorMessage message 45 | 46 | render: (template, data) -> 47 | JST["templates/#{template}"](data) 48 | 49 | $ -> 50 | App.utils.successMessage(App.flash?.success) 51 | App.utils.errorMessage(App.flash?.error) 52 | 53 | $('.online-switch input[type=checkbox]').on 'change', (e) -> 54 | if @checked 55 | App.connect() 56 | else 57 | App.disconnect() 58 | 59 | 60 | App.cable = ActionCable.createConsumer() 61 | 62 | return unless gon.user_id 63 | 64 | notifications = new Notifications() 65 | notifications.on() 66 | 67 | notificationsBtn = $('.notifications-btn') 68 | 69 | notificationsBtn.on 'click', (e) -> 70 | if notificationsBtn.hasClass('is-disabled') 71 | notifications.on() 72 | else 73 | notifications.off() 74 | notificationsBtn.toggleClass('is-disabled') 75 | 76 | -------------------------------------------------------------------------------- /app/assets/javascripts/shared/notifications.coffee: -------------------------------------------------------------------------------- 1 | class window.Notifications 2 | on: -> 3 | return if @active 4 | 5 | @active = true 6 | 7 | @notificationChannel = App.cable.subscriptions.create( 8 | { channel: 'NotificationChannel', id: gon.user_id }, 9 | NotificationChannel 10 | ) 11 | 12 | @notificationChannel.handle_message = (type, data) -> 13 | if type is 'alert' 14 | App.utils.errorMessage(data) 15 | else if type is 'success' 16 | App.utils.successMessage(data) 17 | else 18 | App.utils.simpleMessage(data) 19 | 20 | off: -> 21 | return unless @active 22 | 23 | @active = false 24 | @notificationChannel.unsubscribe() 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/basket.jst.skim: -------------------------------------------------------------------------------- 1 | .basket id="basket_#{@id}" 2 | .col.s12.m7 3 | .card 4 | a href="/baskets/#{@id}" 5 | .card-image 6 | img src=@logo_path 7 | span.card-title =@name 8 | .card-content 9 | p =@description 10 | .card-action 11 | / a.edit-basket-link href="#" Edit 12 | a.delete-basket-link data-remote="true" rel="nofollow" data-method="delete" href="/baskets/#{@id}" Delete 13 | span.badge =@items_count 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/product.jst.skim: -------------------------------------------------------------------------------- 1 | .collection-item id="product_#{@id}" 2 | .product-content 3 | img src=@icon_path 4 | span.title =@name 5 | a.delete-product-link.grey-text.lighten-5-text data-remote="true" rel="nofollow" data-method="delete" href="/products/#{@id}" 6 | i.material-icons delete 7 | span.secondary-content =@category 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.sass: -------------------------------------------------------------------------------- 1 | //= require material_icons 2 | 3 | @import "materialize" 4 | 5 | body 6 | background: white 7 | 8 | .no-events 9 | pointer-events: none 10 | 11 | .ellipses 12 | overflow: hidden 13 | text-overflow: ellipsis 14 | white-space: nowrap 15 | 16 | .nav-wrapper 17 | display: flex 18 | justify-content: space-between 19 | padding: 0 5% 20 | 21 | .nav-left, .nav-right 22 | display: flex 23 | align-items: center 24 | 25 | .nav-button 26 | cursor: pointer 27 | &:hover 28 | opacity: 0.8 29 | 30 | .notifications-btn 31 | color: #7ed321 32 | .inactive 33 | display: none 34 | &.is-disabled 35 | color: #9e9e9e 36 | .active 37 | display: none 38 | .inactive 39 | display: block 40 | 41 | .humanoids 42 | height: 50px 43 | display: flex 44 | > div 45 | width: 50px 46 | 47 | .online-switch 48 | padding: 0 10px 49 | margin-right: 10px 50 | 51 | .baskets-list 52 | display: flex 53 | flex-wrap: wrap 54 | justify-content: space-between 55 | align-items: stretch 56 | 57 | .basket 58 | display: flex 59 | width: 240px 60 | .card-image 61 | width: 100% 62 | height: 120px 63 | background: #f5f5f5 64 | .card-content 65 | height: 120px 66 | overflow: hidden 67 | text-overflow: ellipsis 68 | 69 | .product-content 70 | display: flex 71 | align-items: center 72 | .secondary-content 73 | margin-left: auto 74 | .delete-product-link 75 | opacity: 0.1 76 | &:hover, &:active 77 | .delete-product-link 78 | opacity: 0.7 79 | i 80 | color: #b71c1c 81 | 82 | .delete-product-link 83 | margin-left: 10px 84 | margin-top: 4px 85 | transition: opacity 200ms 86 | i 87 | transition: color 200ms -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. 2 | module ApplicationCable 3 | class Channel < ActionCable::Channel::Base 4 | def current_user 5 | super || 'john' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. 2 | module ApplicationCable 3 | class Connection < ActionCable::Connection::Base 4 | identified_by :current_user 5 | 6 | def connect 7 | self.current_user = verify_user unless Nenv.skip_auth? 8 | end 9 | 10 | def disconnect 11 | ActionCable.server.broadcast( 12 | "notifications", 13 | type: 'alert', data: "#{current_user} disconnected" 14 | ) 15 | end 16 | 17 | private 18 | 19 | def verify_user 20 | cookies[:username].presence || reject_unauthorized_connection 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/channels/baskets_channel.rb: -------------------------------------------------------------------------------- 1 | class BasketsChannel < ApplicationCable::Channel 2 | def follow 3 | stop_all_streams 4 | stream_from "baskets" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/notification_channel.rb: -------------------------------------------------------------------------------- 1 | class NotificationChannel < ApplicationCable::Channel 2 | def follow 3 | stop_all_streams 4 | stream_from "notifications_#{params['id']}" 5 | stream_from "notifications" 6 | transmit type: 'notice', data: "Welcome, #{current_user}!" 7 | end 8 | 9 | def unsubscribed 10 | # Wow! Action Cable cannot handle this( 11 | # transmit type: 'success', data: 'Notifications turned off. Good-bye!' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/channels/products_channel.rb: -------------------------------------------------------------------------------- 1 | class ProductsChannel < ApplicationCable::Channel 2 | def follow(data) 3 | stop_all_streams 4 | stream_from "baskets/#{data['id']}" 5 | end 6 | 7 | def unsubscribed 8 | ActionCable.server.broadcast( 9 | "notifications", 10 | type: 'notice', data: "#{current_user} left this basket page" 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | before_action :require_login 7 | after_action :broadcast_changes, only: [:create, :destroy] 8 | 9 | helper_method :current_user 10 | helper_method :logged_in? 11 | 12 | include Serialized 13 | 14 | def current_user 15 | @current_user ||= (session[:username] || cookies[:username]) 16 | end 17 | 18 | def logged_in? 19 | current_user.present? 20 | end 21 | 22 | def broadcast_changes 23 | return if resource.errors.any? 24 | ActionCable.server.broadcast channel_name, channel_message 25 | end 26 | 27 | protected 28 | 29 | def require_login 30 | redirect_to(login_path) unless logged_in? 31 | gon.user_id = current_user 32 | end 33 | 34 | def channel_name 35 | controller_name 36 | end 37 | 38 | def channel_message 39 | { type: action_name, data: resource.serialized(adapter: :attributes).as_json } 40 | end 41 | 42 | def resource 43 | @resource ||= instance_variable_get("@#{controller_name.singularize}") 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/baskets_controller.rb: -------------------------------------------------------------------------------- 1 | class BasketsController < ApplicationController 2 | before_action :set_basket, only: [:show, :destroy, :update] 3 | 4 | def index 5 | @baskets = Basket.all 6 | end 7 | 8 | def show 9 | gon.basket_id = @basket.id 10 | @products = @basket.products 11 | end 12 | 13 | def create 14 | @basket = Basket.create(basket_params.merge(owner: current_user)) 15 | render_json @basket 16 | end 17 | 18 | def destroy 19 | @basket.destroy! 20 | render_json_message 21 | end 22 | 23 | private 24 | 25 | def basket_params 26 | params.require(:basket).permit(:name, :logo_path, :description) 27 | end 28 | 29 | def set_basket 30 | @basket = Basket.find(params[:id]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/serialized.rb: -------------------------------------------------------------------------------- 1 | module Serialized 2 | extend ActiveSupport::Concern 3 | 4 | def render_json(item, root_name = controller_name.singularize) 5 | if item.errors.any? 6 | render_errors item 7 | else 8 | render json: item, root: root_name, meta_key: :message, meta: t('.message') 9 | end 10 | end 11 | 12 | def render_json_message 13 | render json: { message: t('.message') } 14 | end 15 | 16 | def render_errors(object) 17 | render text: object.errors.full_messages.join("
"), status: :forbidden 18 | end 19 | 20 | def render_not_found 21 | render status: :not_found 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/products_controller.rb: -------------------------------------------------------------------------------- 1 | class ProductsController < ApplicationController 2 | before_action :set_basket, only: [:create] 3 | 4 | def create 5 | @product = @basket.products.create(product_params) 6 | render_json @product 7 | broadcast_products_update(@basket) 8 | end 9 | 10 | def destroy 11 | @product = Product.find(params[:id]) 12 | @product.destroy! 13 | render_json_message 14 | broadcast_products_update(@product.basket) 15 | end 16 | 17 | private 18 | 19 | def channel_name 20 | "baskets/#{@product.basket_id}" 21 | end 22 | 23 | def product_params 24 | params.require(:product).permit(:name, :category) 25 | end 26 | 27 | def set_basket 28 | @basket = Basket.find(params[:basket_id]) 29 | end 30 | 31 | def broadcast_products_update(basket) 32 | ActionCable.server.broadcast 'baskets', { 33 | type: 'products-update', 34 | data: { 35 | basket_id: basket.id, 36 | count: basket.products.count 37 | } 38 | } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | skip_before_action :require_login, only: [:new, :create] 3 | skip_after_action :broadcast_changes 4 | 5 | def new 6 | @session_form = SessionForm.new(username: Faker::Internet.user_name) 7 | end 8 | 9 | def create 10 | @session_form = SessionForm.new(session_params) 11 | if @session_form.valid? 12 | reset_session 13 | session[:username] = @session_form.username 14 | cookies[:username] = { value: @session_form.username, domain: :all } 15 | redirect_to root_path 16 | else 17 | render :new 18 | end 19 | end 20 | 21 | def destroy 22 | reset_session 23 | cookies.delete(:username) 24 | redirect_to login_path 25 | end 26 | 27 | private 28 | 29 | def session_params 30 | params.require(:session_form).permit(:username) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/forms/session_form.rb: -------------------------------------------------------------------------------- 1 | class SessionForm 2 | include ActiveModel::Model 3 | 4 | attr_accessor :username 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def humanoids 3 | <<-HTML 4 |
5 | HTML 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | 4 | def serialized(options = {}) 5 | ActiveModelSerializers::SerializableResource.new(self, options) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/basket.rb: -------------------------------------------------------------------------------- 1 | require "faker/food" 2 | 3 | class Basket < ApplicationRecord 4 | before_create :set_defaults 5 | 6 | has_many :products, dependent: :destroy 7 | 8 | private 9 | 10 | def set_defaults 11 | self.name ||= Faker::Hipster.word 12 | self.description ||= Faker::Hipster.paragraph 13 | self.owner ||= Faker::Internet.user_name 14 | self.logo_path ||= "https://unsplash.it/400/200?image=#{rand(1000)}" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/product.rb: -------------------------------------------------------------------------------- 1 | require "faker/food" 2 | 3 | class Product < ApplicationRecord 4 | CATEGORIES = %w(FRUIT VEGGIE BERRY MEAT FISH GAME HERB).freeze 5 | 6 | before_create :set_defaults 7 | 8 | belongs_to :basket, counter_cache: :items_count 9 | 10 | private 11 | 12 | def set_defaults 13 | self.name ||= Faker::Food.name 14 | self.icon_path ||= Faker::Food.icon 15 | self.category ||= CATEGORIES.sample 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/serializers/basket_serializer.rb: -------------------------------------------------------------------------------- 1 | class BasketSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :logo_path, :description, :owner, :items_count 3 | end 4 | -------------------------------------------------------------------------------- /app/serializers/product_serializer.rb: -------------------------------------------------------------------------------- 1 | class ProductSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :category, :icon_path, :basket_id 3 | end 4 | -------------------------------------------------------------------------------- /app/views/baskets/_basket.slim: -------------------------------------------------------------------------------- 1 | .basket id=dom_id(basket) 2 | .col.s12.m7 3 | .card 4 | a href=basket_path(basket) 5 | .card-image 6 | img src=basket.logo_path 7 | span.card-title =basket.name 8 | .card-content 9 | p =basket.description 10 | .card-action 11 | / a.edit-basket-link href="#" Edit 12 | a.delete-basket-link data-remote="true" rel="nofollow" data-method="delete" href=basket_path(basket) Delete 13 | span.badge =basket.items_count 14 | 15 | -------------------------------------------------------------------------------- /app/views/baskets/_form.slim: -------------------------------------------------------------------------------- 1 | #basket_form style="display: none;" 2 | = form_for Basket.new, remote: true do |f| 3 | .row 4 | .input-field.col.s12 5 | =f.text_field :name 6 | =f.label :name 7 | .row 8 | .input-field.col.s12 9 | =f.label :description 10 | =f.text_area :description, class: 'materialize-textarea' 11 | .row 12 | .col 13 | =f.submit "Save", class: 'waves-effect waves-light btn' 14 | .col 15 | .btn.cancel-btn.grey.waves-effect.waves-light.lighten-4.black-text Cancel 16 | -------------------------------------------------------------------------------- /app/views/baskets/index.slim: -------------------------------------------------------------------------------- 1 | h3 Baskets 2 | 3 | .baskets-list#baskets_list 4 | - if @baskets.any? 5 | = render @baskets 6 | - else 7 | div 8 | .col.s6.offset-s3.center No baskets found( 9 | 10 | = render 'shared/add_btn', id: "add_basket_btn" 11 | = render 'form' 12 | 13 | = content_for :scripts 14 | = javascript_include_tag 'baskets' 15 | -------------------------------------------------------------------------------- /app/views/baskets/show.slim: -------------------------------------------------------------------------------- 1 | .collection#products_list 2 | - if @products.any? 3 | = render @products 4 | - else 5 | div 6 | .col.s6.offset-s3.center No Products( 7 | 8 | = render 'shared/add_btn', id: "add_product_btn" 9 | = render 'products/form' 10 | 11 | = content_for :scripts 12 | = javascript_include_tag 'products' 13 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title AnyCable Demo 5 | = Gon::Base.render_data 6 | = stylesheet_link_tag 'application', media: 'all' 7 | = action_cable_meta_tag 8 | = csrf_meta_tags 9 | body 10 | .navbar-fixed 11 | nav 12 | .nav-wrapper.light-blue 13 | .nav-left 14 | == humanoids 15 | = link_to 'AnyCable', root_path, class: 'brand-logo' 16 | .nav-right 17 | - if logged_in? 18 | .switch.online-switch 19 | label.white-text 20 | | Off 21 | input type="checkbox" checked=true 22 | span.lever 23 | | On 24 | span.username ="@#{current_user}" 25 | = image_tag Faker::Avatar.image(current_user, "50x50") 26 | .nav-button.notifications-btn#notifications_btn 27 | i.material-icons.active network_wifi 28 | i.material-icons.inactive signal_wifi_off 29 | .container.main 30 | = yield 31 | 32 | .footer 33 | = Gon::Base.render_data 34 | - if Rails.env.test? 35 | = javascript_include_tag 'phantom' 36 | = javascript_include_tag 'application' 37 | = yield :scripts 38 | -------------------------------------------------------------------------------- /app/views/products/_form.slim: -------------------------------------------------------------------------------- 1 | #product_form style="display: none;" 2 | = form_for [@basket, @basket.products.new], remote: true do |f| 3 | .row 4 | .input-field.col.s12 5 | =f.text_field :name 6 | =f.label :name 7 | .row 8 | .input-field.col.s12 9 | =f.select :category, Product::CATEGORIES 10 | =f.label :category 11 | .row 12 | .col 13 | =f.submit "Save", class: 'waves-effect waves-light btn' 14 | .col 15 | .btn.cancel-btn.grey.waves-effect.waves-light.lighten-4.black-text Cancel 16 | -------------------------------------------------------------------------------- /app/views/products/_product.slim: -------------------------------------------------------------------------------- 1 | .collection-item id=dom_id(product) 2 | .product-content 3 | img src=product.icon_path 4 | span.title =product.name 5 | a.delete-product-link.grey-text.lighten-5-text data-remote="true" rel="nofollow" data-method="delete" href=product_path(product) 6 | i.material-icons delete 7 | span.secondary-content =product.category 8 | -------------------------------------------------------------------------------- /app/views/sessions/new.slim: -------------------------------------------------------------------------------- 1 | h2 Log in 2 | 3 | = form_for(@session_form, url: login_path) do |f| 4 | .row 5 | .col.input-field.s12 6 | = f.text_field :username, autofocus: true, class: 'validate' 7 | = f.label :username 8 | .row 9 | .col.s12 10 | = f.submit "Log in", class: 'waves-effect waves-light btn' 11 | -------------------------------------------------------------------------------- /app/views/shared/_add_btn.slim: -------------------------------------------------------------------------------- 1 | .fixed-action-btn style="bottom: 45px; right: 24px;" id="#{ local_assigns.fetch(:id, nil) }" 2 | = link_to '#', class: "btn-floating btn-large red" do 3 | i.large.material-icons add -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/cable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | bundle exec puma -p 28080 cable/config.ru --pidfile tmp/pids/cable.pid -w 4 -t 32:32 -------------------------------------------------------------------------------- /bin/rackup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rackup' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rack", "rackup") 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rspec' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rspec-core", "rspec") 18 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'spring' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("spring", "spring") 18 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /cable/config.ru: -------------------------------------------------------------------------------- 1 | require ::File.expand_path('../../config/environment', __FILE__) 2 | Rails.application.eager_load! 3 | 4 | run ActionCable.server 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | # require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | require "sprockets/railtie" 13 | 14 | # Require the gems listed in Gemfile, including any gems 15 | # you've limited to :test, :development, or :production. 16 | Bundler.require(*Rails.groups) 17 | 18 | module AnyCableDemo 19 | class Application < Rails::Application 20 | # Settings in config/environments/* take precedence over those specified here. 21 | # Application configuration should go into files in config/initializers 22 | # -- all .rb files in that directory are automatically loaded. 23 | config.time_zone = "Berlin" 24 | config.action_cable.disable_request_forgery_protection = true 25 | config.action_cable.url = Nenv.cable_url 26 | config.action_cable.mount_path = Nenv.cable_url? ? nil : "/cable" 27 | 28 | config.autoload_paths += %W(#{config.root}/lib) 29 | 30 | config.active_record.sqlite3.represent_boolean_as_integer = true 31 | 32 | if ENV["ANYCABLE_RACK"] 33 | config.any_cable_rack.run_rpc = true 34 | end 35 | 36 | # add all upper level assets 37 | config.assets.precompile += 38 | Dir[Rails.root.join('app/assets/*/*.{js,css,coffee,sass,scss}*')] 39 | .map { |i| File.basename(i).sub(/(\.js)?\.coffee$/, '.js') } 40 | .map { |i| File.basename(i).sub(/(\.css)?\.(sass|scss)$/, '.css') } 41 | .reject { |i| i =~ /^application\.(js|css)$/ } 42 | 43 | config.generators do |g| 44 | g.assets false 45 | g.helper false 46 | g.orm :active_record 47 | g.template_engine :slim 48 | g.stylesheets false 49 | g.javascripts false 50 | g.test_framework false 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | # Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket. 2 | # production: 3 | # adapter: redis 4 | # url: redis://localhost:6379/1 5 | 6 | production: 7 | adapter: async 8 | 9 | development: 10 | adapter: <%= ENV.fetch('ADAPTER', 'async') %> 11 | 12 | test: 13 | adapter: <%= ENV.fetch('ADAPTER', 'async') %> 14 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: sqlite3 3 | encoding: unicode 4 | # user: redspread 5 | # password: redspread 6 | # For details on connection pooling, see rails configuration guide 7 | # http://guides.rubyonrails.org/configuring.html#database-pooling 8 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 9 | 10 | development: &development 11 | <<: *default 12 | database: db/dev.sqlite3 13 | 14 | test: 15 | <<: *default 16 | database: db/test.sqlite3 17 | 18 | production: 19 | <<: *development 20 | adapter: postgresql 21 | database: anycable_demo_db 22 | pool: 5 23 | timeout: 5000 24 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | require 'faker/food' 5 | require 'anycable-rails' if Nenv.cable_url? 6 | 7 | # Initialize the Rails application. 8 | Rails.application.initialize! 9 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Print deprecation notices to the Rails logger. 30 | config.active_support.deprecation = :log 31 | 32 | # Raise an error on page load if there are pending migrations. 33 | config.active_record.migration_error = :page_load 34 | 35 | # Debug mode disables concatenation and preprocessing of assets. 36 | # This option may cause significant delays in view rendering with a large 37 | # number of complex assets. 38 | config.assets.debug = true 39 | 40 | config.assets.quiet = true 41 | 42 | # Raises error for missing translations 43 | # config.action_view.raise_on_missing_translations = true 44 | 45 | # Use an evented file watcher to asynchronously detect changes in source code, 46 | # routes, locales, etc. This feature depends on the listen gem. 47 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 48 | end 49 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = true 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Action Cable endpoint configuration 38 | # config.action_cable.url = 'wss://example.com/cable' 39 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 40 | 41 | # Don't mount Action Cable in the main server process. 42 | # config.action_cable.mount_path = nil 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use a different cache store in production. 48 | # config.cache_store = :mem_cache_store 49 | 50 | # Use a real queuing backend for Active Job (and separate queues per environment) 51 | # config.active_job.queue_adapter = :resque 52 | # config.active_job.queue_name_prefix = "any_cable_#{Rails.env}" 53 | 54 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 55 | # the I18n.default_locale when a translation cannot be found). 56 | config.i18n.fallbacks = [I18n.default_locale] 57 | 58 | # Send deprecation notices to registered listeners. 59 | config.active_support.deprecation = :notify 60 | 61 | # Use default logging formatter so that PID and timestamp are not suppressed. 62 | config.log_formatter = ::Logger::Formatter.new 63 | 64 | logger = ActiveSupport::Logger.new(STDOUT) 65 | logger.formatter = config.log_formatter 66 | config.logger = ActiveSupport::TaggedLogging.new(logger) 67 | config.action_view.logger = nil 68 | config.log_level = :info 69 | config.log_tags = [ :request_id ] 70 | 71 | # Do not dump schema after migrations. 72 | config.active_record.dump_schema_after_migration = false 73 | end 74 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Print deprecation notices to the stderr. 32 | config.active_support.deprecation = :stderr 33 | 34 | # Raises error for missing translations 35 | # config.action_view.raise_on_missing_translations = true 36 | config.middleware.use RackSessionAccess::Middleware 37 | end 38 | -------------------------------------------------------------------------------- /config/initializers/active_model_serializers.rb: -------------------------------------------------------------------------------- 1 | ActiveModelSerializers.config.adapter = :json -------------------------------------------------------------------------------- /config/initializers/active_record_belongs_to_required_by_default.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Require `belongs_to` associations by default. This is a new Rails 5.0 4 | # default, so it is introduced as a configuration option to ensure that apps 5 | # made on earlier versions of Rails are not affected when upgrading. 6 | Rails.application.config.active_record.belongs_to_required_by_default = true 7 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | Rails.application.config.assets.precompile += %w( phantom.js ) 7 | 8 | # Add additional assets to the asset load path 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 13 | # Rails.application.config.assets.precompile += %w( search.js ) 14 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 2 | allow do 3 | origins '*' 4 | 5 | resource '*', 6 | headers: :any, 7 | methods: [:get, :post, :put, :patch, :delete, :options, :head] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/faker.rb: -------------------------------------------------------------------------------- 1 | require 'faker/food' 2 | 3 | module Faker 4 | class Food 5 | ICONS = [ 6 | 'apple', 7 | 'asparagus', 8 | 'bacon', 9 | 'banana', 10 | 'banana_split', 11 | 'bar', 12 | 'beer', 13 | 'beer_bottle', 14 | 'beer_can', 15 | 'beer_glass', 16 | 'beer_keg', 17 | 'beer_pump', 18 | 'beet', 19 | 'bento', 20 | 'big_green_egg', 21 | 'birthday_cake', 22 | 'bottle_of_water', 23 | 'bread', 24 | 'cabbage', 25 | 'cafe', 26 | 'caloric_energy', 27 | 'carbohydrates', 28 | 'carrot', 29 | 'celery', 30 | 'charcoal', 31 | 'cheese', 32 | 'cherry', 33 | 'cinnamon_roll', 34 | 'cocktail', 35 | 'coffee_pot', 36 | 'coffee_to_go', 37 | 'cooker', 38 | 'cooker_hood', 39 | 'cookies', 40 | 'cooking_pot', 41 | 'corkscrew', 42 | 'corn', 43 | 'cucumber', 44 | 'cup', 45 | 'cupcake', 46 | 'deliver_food', 47 | 'dim_sum', 48 | 'dolmades', 49 | 'doughnut', 50 | 'eggplant', 51 | 'eggs', 52 | 'espresso_cup', 53 | 'fiber', 54 | 'fish_food', 55 | 'food_and_wine', 56 | 'fork', 57 | 'french_fries', 58 | 'french_press', 59 | 'fridge', 60 | 'garlic', 61 | 'gravy_boat', 62 | 'grill', 63 | 'hamburger', 64 | 'honey', 65 | 'hot_chocolate', 66 | 'hot_dog', 67 | 'ice_cream_bowl', 68 | 'ice_cream_cone', 69 | 'ice_cream_scoop', 70 | 'ingredients', 71 | 'ingredients_list', 72 | 'kebab', 73 | 'kiwi', 74 | 'knife', 75 | 'kohlrabi', 76 | 'ladle', 77 | 'leek', 78 | 'lettuce', 79 | 'lipids', 80 | 'low_salt', 81 | 'macaron', 82 | 'meal', 83 | 'melon', 84 | 'microwave', 85 | 'milk', 86 | 'milk_bottle', 87 | 'mixer', 88 | 'mushroom', 89 | 'nachos', 90 | 'natural_food', 91 | 'no_GMO', 92 | 'no_apple', 93 | 'no_celery', 94 | 'no_crustaceans', 95 | 'no_eggs', 96 | 'no_fish', 97 | 'no_fructose', 98 | 'no_gluten', 99 | 'no_lupines', 100 | 'no_milk', 101 | 'no_mustard', 102 | 'no_nuts', 103 | 'no_peanut', 104 | 'no_sesame', 105 | 'no_shellfish', 106 | 'no_soy', 107 | 'no_sugar', 108 | 'no_sugar2', 109 | 'nonya_kueh', 110 | 'noodles', 111 | 'olive', 112 | 'olive_oil', 113 | 'organic_food', 114 | 'pancake', 115 | 'pastry_bag', 116 | 'pastry_spatula', 117 | 'peach', 118 | 'peas', 119 | 'pepper_shaker', 120 | 'pie', 121 | 'pizza', 122 | 'plum', 123 | 'porridge', 124 | 'protein', 125 | 'quesadilla', 126 | 'rack_of_lamb', 127 | 'radish', 128 | 'raspberry', 129 | 'restaurant_pickup', 130 | 'rice_bowl', 131 | 'sack_of_flour', 132 | 'salt_shaker', 133 | 'sauce', 134 | 'sesame', 135 | 'shellfish', 136 | 'soda_bottle', 137 | 'sodium', 138 | 'spaghetti', 139 | 'spoon', 140 | 'spoon_of_sugar', 141 | 'steak', 142 | 'sugar', 143 | 'sugar_cube', 144 | 'sugar_cubes', 145 | 'sweet_potato', 146 | 'sweetener', 147 | 'taco', 148 | 'tapas', 149 | 'tea', 150 | 'tea_cup', 151 | 'teapot', 152 | 'tin_can', 153 | 'vegan_food', 154 | 'vegan_symbol', 155 | 'vegetarian_food', 156 | 'waiter', 157 | 'weber', 158 | 'wine_bottle', 159 | 'wine_glass', 160 | 'wooden_beer_keg', 161 | 'wrap' 162 | ].freeze 163 | 164 | def self.icon(size = 48) 165 | "https://maxcdn.icons8.com/Color/PNG/#{size}/Food/#{ICONS.sample}-#{size}.png" 166 | end 167 | 168 | def self.name 169 | ICONS.sample.humanize 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/per_form_csrf_tokens.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Enable per-form CSRF tokens. 4 | Rails.application.config.action_controller.per_form_csrf_tokens = true 5 | -------------------------------------------------------------------------------- /config/initializers/request_forgery_protection.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Enable origin-checking CSRF mitigation. 4 | Rails.application.config.action_controller.forgery_protection_origin_check = true 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | if Rails.env.production? 3 | Rails.application.config.session_store :cookie_store, 4 | key: '_any_cable_session', 5 | domain: :all 6 | else 7 | Rails.application.config.session_store :cookie_store, key: '_any_cable_session' 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/ssl_options.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure SSL options to enable HSTS with subdomains. 4 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 5 | -------------------------------------------------------------------------------- /config/initializers/to_time_preserves_timezone.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Preserve the timezone of the receiver when calling to `to_time`. 4 | # Ruby 2.4 will change the behavior of `to_time` to preserve the timezone 5 | # when converting to an instance of `Time` instead of the previous behavior 6 | # of converting to the local system timezone. 7 | # 8 | # Rails 5.0 introduced this config option so that apps made with earlier 9 | # versions of Rails are not affected when upgrading. 10 | ActiveSupport.to_time_preserves_timezone = true 11 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/initializers/yabeda.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "anycable/middleware" 4 | 5 | # FIXME 6 | return 7 | 8 | class MetricsMiddleware < AnyCable::Middleware 9 | BUCKETS = [ 10 | 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 11 | ].freeze 12 | 13 | # handler - is a method (Method object) of RPC handler which is called 14 | # rpc_call - is an active gRPC call 15 | # request - is a request payload (incoming message) 16 | def call(request, rpc_call, handler) 17 | labels = { method: handler.name } 18 | start = Time.now 19 | begin 20 | yield 21 | Yabeda.anycable_rpc_success_total.increment(labels) 22 | rescue Exception # rubocop: disable Lint/RescueException 23 | Yabeda.anycable_rpc_failed_total.increment(labels) 24 | raise 25 | ensure 26 | time = elapsed(start) 27 | Yabeda.anycable_rpc_runtime.measure(labels, time) 28 | Yabeda.anycable_rpc_executed_total.increment(labels) 29 | end 30 | end 31 | 32 | private 33 | 34 | def elapsed(start) 35 | (Time.now - start).round(3) 36 | end 37 | end 38 | 39 | Yabeda.configure do 40 | group :anycable 41 | 42 | counter :rpc_executed_total, comment: "Total number of rpc calls" 43 | counter :rpc_success_total, comment: "Total number of successfull rpc calls" 44 | counter :rpc_failed_total, comment: "Total number of failed rpc calls" 45 | histogram :rpc_runtime, comment: "RPC runtime", unit: :seconds, per: :method, 46 | buckets: MetricsMiddleware::BUCKETS 47 | end 48 | 49 | AnyCable.configure_server do 50 | AnyCable.middleware.use(MetricsMiddleware) 51 | 52 | Yabeda::Prometheus::Exporter.start_metrics_server! 53 | end 54 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | baskets: 24 | create: 25 | message: "Your basket has been successfully created" 26 | destroy: 27 | message: "Your basket has been successfully removed" 28 | update: 29 | message: "Your basket has been successfully updated" 30 | products: 31 | create: 32 | message: "Your product has been successfully created" 33 | destroy: 34 | message: "Your product has been successfully removed" 35 | update: 36 | message: "Your product has been successfully updated" -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :items 3 | root to: 'baskets#index' 4 | 5 | get '/login' => 'sessions#new', as: :login 6 | post '/login' => 'sessions#create' 7 | 8 | get '/logout' => 'sessions#destroy', as: :logout 9 | 10 | resources :baskets, only: [:index, :show, :create, :update, :destroy] do 11 | resources :products, only: [:create, :update, :destroy], shallow: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: d4ff1264f0beb989ef645a9498d5e6a6dc5d8a6cbb73985b11fe9ed1db20df77d0e2eab8a6aa6f0a9f94092c53d5d7699e735d76ed1f72b10fcfd74b9da93b54 15 | anycable: 16 | access_logs_disabled: false 17 | 18 | test: 19 | secret_key_base: 0a1f23b865641a22678edddd7a9029143a9b000ea83ba115914df79ce45ea978ccec71f1f14cbe69c14871327fe93e6501fc570de1d79c90d2f63c020ed0b021 20 | 21 | # Do not keep production secrets in the repository, 22 | # instead read values from the environment. 23 | production: 24 | secret_key_base: d4ff1264f0beb989ef645a9498d5e6a6dc5d8a6cbb73985b11fe9ed1db20df77d0e2eab8a6aa6f0a9f94092c53d5d7699e735d76ed1f72b10fcfd74b9da93b54 25 | anycable: 26 | access_logs_disabled: false 27 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20160620113623_create_baskets.rb: -------------------------------------------------------------------------------- 1 | class CreateBaskets < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :baskets do |t| 4 | t.string :name 5 | t.string :logo_path 6 | t.string :owner 7 | t.text :description 8 | t.integer :items_count, null: false, default: 0 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20160621123902_create_products.rb: -------------------------------------------------------------------------------- 1 | class CreateProducts < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :products do |t| 4 | t.string :name 5 | t.string :icon_path 6 | t.string :category 7 | t.belongs_to(:basket, index: true) 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2016_06_21_123902) do 14 | 15 | create_table "baskets", force: :cascade do |t| 16 | t.string "name" 17 | t.string "logo_path" 18 | t.string "owner" 19 | t.text "description" 20 | t.integer "items_count", default: 0, null: false 21 | t.datetime "created_at", null: false 22 | t.datetime "updated_at", null: false 23 | end 24 | 25 | create_table "products", force: :cascade do |t| 26 | t.string "name" 27 | t.string "icon_path" 28 | t.string "category" 29 | t.integer "basket_id" 30 | t.datetime "created_at", null: false 31 | t.datetime "updated_at", null: false 32 | t.index ["basket_id"], name: "index_products_on_basket_id" 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | 3.times { Basket.create! } 9 | 10 | baskets = Basket.all 11 | 12 | 3.times { baskets.first.products.create } 13 | 2.times { baskets.second.products.create } 14 | 4.times { baskets.last.products.create } 15 | -------------------------------------------------------------------------------- /dip.yml: -------------------------------------------------------------------------------- 1 | version: '4.1' 2 | 3 | environment: 4 | RAILS_ENV: development 5 | 6 | compose: 7 | files: 8 | - docker-compose.yml 9 | project_name: anycable_demo 10 | 11 | interaction: 12 | sh: 13 | description: Open a Bash shell within a Rails container (with dependencies up) 14 | service: web 15 | command: /bin/bash 16 | 17 | bundle: 18 | description: Run Bundler commands 19 | service: web 20 | command: bundle 21 | compose_run_options: [no-deps] 22 | 23 | rake: 24 | description: Run Rake commands 25 | service: web 26 | command: bundle exec rake 27 | 28 | rails: 29 | description: Run Rails commands 30 | service: web 31 | command: bundle exec rails 32 | subcommands: 33 | s: 34 | description: Run Rails server available at http://localhost:3000 35 | service: web 36 | compose: 37 | run_options: [service-ports, use-aliases] 38 | c: 39 | description: Run Rails console 40 | service: web 41 | command: bundle exec rails console 42 | 43 | rspec: 44 | description: Run Rails tests 45 | service: web 46 | environment: 47 | RAILS_ENV: test 48 | command: bundle exec rspec 49 | 50 | rubocop: 51 | description: Run Rubocop 52 | service: web 53 | command: bundle exec rubocop 54 | compose_run_options: [no-deps] 55 | 56 | provision: 57 | - dip compose down --volumes 58 | - dip compose up -d redis 59 | - dip bundle install 60 | - dip rails db:prepare 61 | - dip rails db:seed 62 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | x-app-service-template: &app 4 | image: anycable-demo:0.6.6 5 | build: 6 | context: . 7 | dockerfile: .dockerdev/Dockerfile 8 | args: 9 | RUBY_VERSION: "2.6.5" 10 | NODE_MAJOR: '12' 11 | BUNDLER_VERSION: '1.17.1' 12 | tmpfs: 13 | - /tmp 14 | environment: &env 15 | NODE_ENV: development 16 | RAILS_ENV: ${RAILS_ENV:-development} 17 | ANYCABLE_DEBUG: ${DEBUG:-0} 18 | ANYCABLE_RPC_HOST: 0.0.0.0:50051 19 | 20 | x-backend: &backend 21 | <<: *app 22 | volumes: 23 | - .:/app:cached 24 | - rails_cache:/app/tmp/cache 25 | - bundle:/usr/local/bundle 26 | - .dockerdev/.pryrc:/root/.pryrc:ro 27 | - .dockerdev/.bashrc:/root/.bashrc:ro 28 | tmpfs: 29 | - /app/tmp/pids 30 | environment: 31 | <<: *env 32 | REDIS_URL: redis://redis:6379/ 33 | WEB_CONCURRENCY: 1 34 | HISTFILE: /app/tmp/cache/.bash_history 35 | EDITOR: vi 36 | CABLE_URL: ws://localhost:8080/cable 37 | ADAPTER: any_cable 38 | stdin_open: true 39 | tty: true 40 | depends_on: 41 | redis: 42 | condition: service_healthy 43 | 44 | services: 45 | web: 46 | <<: *backend 47 | command: bundle exec rails server -b 0.0.0.0 48 | ports: 49 | - '3000:3000' 50 | 51 | rpc: 52 | <<: *backend 53 | command: bundle exec anycable 54 | 55 | redis: 56 | image: redis:5.0-alpine 57 | volumes: 58 | - redis:/data 59 | ports: 60 | - 6379 61 | healthcheck: 62 | test: redis-cli ping 63 | interval: 1s 64 | timeout: 3s 65 | retries: 30 66 | 67 | anycable: 68 | image: 'anycable/anycable-go:1.0.0.preview1' 69 | ports: 70 | - "8080:8080" 71 | environment: 72 | PORT: 8080 73 | ANYCABLE_HOST: 0.0.0.0 74 | REDIS_URL: redis://redis:6379/0 75 | ANYCABLE_RPC_HOST: rpc:50051 76 | ANYCABLE_DEBUG: ${DEBUG:-0} 77 | depends_on: 78 | - redis 79 | - rpc 80 | 81 | volumes: 82 | redis: 83 | bundle: 84 | assets: 85 | rails_cache: 86 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/heroku.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | 5 | namespace :heroku do 6 | task :release do 7 | # The name or ID of the connected RPC app 8 | rpc_app = ENV["HEROKU_ANYCABLE_RPC_APP"] 9 | # We retrieve the name of the current app from the Heroku metadata. 10 | # NOTE: You must enable runtime-dyno-metadata feature. See https://devcenter.heroku.com/articles/dyno-metadata 11 | current_app = ENV["HEROKU_APP_ID"] 12 | next unless rpc_app && current_app 13 | 14 | # Use Heroku CLI to generate a token: 15 | # heroku auth:token 16 | token = ENV.fetch("HEROKU_TOKEN") 17 | 18 | fetch_config = proc do |app| 19 | uri = URI.parse("https://api.heroku.com/apps/#{app}/config-vars") 20 | header = { 21 | "Accept": "application/vnd.heroku+json; version=3", 22 | "Authorization": "Bearer #{ENV.fetch("HEROKU_TOKEN")}" 23 | } 24 | 25 | http = Net::HTTP.new(uri.host, uri.port) 26 | req = Net::HTTP::Get.new(uri.request_uri, header) 27 | 28 | res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| 29 | http.request(req) 30 | end 31 | 32 | raise "Failed to fetch config vars for the current app: #{res.value}\n#{res.body}" unless res.is_a?(Net::HTTPSuccess) 33 | 34 | JSON.parse(res.body).tap do |config| 35 | # remove all Heroku metadata 36 | config.delete_if { |k, _| k.start_with?("HEROKU_") } 37 | end 38 | end 39 | 40 | current_config = fetch_config.(current_app).tap do |config| 41 | # remove protected RPC vars we don't want to sync 42 | %w().each { |k| config.delete(k) } 43 | end 44 | 45 | rpc_config = fetch_config.(rpc_app).tap do |config| 46 | # remove protected RPC vars we don't want to update/remove 47 | %w(ANYCABLE_DEPLOYMENT).each { |k| config.delete(k) } 48 | end 49 | 50 | keys_to_delete = rpc_config.keys - current_config.keys 51 | missing_keys = current_config.keys - rpc_config.keys 52 | sync_keys = (current_config.keys - missing_keys).select { |k| current_config[k] != rpc_config[k] } 53 | 54 | $stdout.puts "RPC only keys: #{keys_to_delete.join(", ")}" unless keys_to_delete.empty? 55 | $stdout.puts "APP only keys: #{missing_keys.join(", ")}" unless missing_keys.empty? 56 | $stdout.puts "Out-of-sync keys: #{sync_keys.join(", ")}" unless sync_keys.empty? 57 | 58 | payload = Hash[keys_to_delete.map { |k| [k, nil] } + (sync_keys + missing_keys).map { |k| [k, current_config[k]] }] 59 | 60 | uri = URI.parse("https://api.heroku.com/apps/#{rpc_app}/config-vars") 61 | header = { 62 | "Accept": "application/vnd.heroku+json; version=3", 63 | "Content-Type": "application/json", 64 | "Authorization": "Bearer #{ENV.fetch("HEROKU_TOKEN")}" 65 | } 66 | 67 | http = Net::HTTP.new(uri.host, uri.port) 68 | req = Net::HTTP::Patch.new(uri.request_uri, header) 69 | req.body = payload.to_json 70 | 71 | res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| 72 | http.request(req) 73 | end 74 | 75 | raise "Failed to update config vars for the RPC app: #{res.value}\n#{res.body}" unless res.is_a?(Net::HTTPSuccess) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tasks/rpc.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :rpc do 4 | desc "Make test GRPC call" 5 | task check: :environment do 6 | require 'grpc' 7 | GRPC::DefaultLogger::LOGGER = Logger.new(STDOUT) 8 | stub = Anycable::Connector::Stub.new('localhost:50051', :this_channel_is_insecure) 9 | stub.connect(Anycable::ConnectionRequest.new(path: 'qwe', headers: { 'cookie' => 'qweqw' })) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /spec/acceptance/create_basket_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'acceptance_helper' 4 | 5 | feature 'create basket', :js do 6 | context "as user" do 7 | before do 8 | sign_in('john') 9 | visit baskets_path 10 | end 11 | 12 | scenario 'creates basket' do 13 | page.find("#add_basket_btn").click 14 | 15 | within "#basket_form" do 16 | fill_in 'Name', with: 'Test basket' 17 | fill_in 'Description', with: 'Test text' 18 | click_on 'Save' 19 | end 20 | 21 | expect(page).to have_content I18n.t('baskets.create.message') 22 | expect(page).to have_content 'Test basket' 23 | expect(page).to have_content 'Test text' 24 | end 25 | end 26 | 27 | context "multiple sessions" do 28 | scenario "all users see new basket in real-time" do 29 | Capybara.using_session(:a) do 30 | sign_in('john') 31 | visit baskets_path 32 | end 33 | 34 | Capybara.using_session(:b) do 35 | sign_in('jack') 36 | visit baskets_path 37 | expect(page).to have_content 'No baskets found(' 38 | end 39 | 40 | Capybara.using_session(:a) do 41 | page.find("#add_basket_btn").click 42 | 43 | within "#basket_form" do 44 | fill_in 'Name', with: 'Test basket' 45 | fill_in 'Description', with: 'Test text' 46 | click_on 'Save' 47 | end 48 | 49 | expect(page).to have_content I18n.t('baskets.create.message') 50 | expect(page).to have_content 'Test basket' 51 | expect(page).to have_content 'Test text' 52 | end 53 | 54 | Capybara.using_session(:b) do 55 | expect(page).to have_content 'Test basket' 56 | expect(page).to_not have_content 'No baskets found(' 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/acceptance/create_product_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'acceptance_helper' 4 | 5 | feature 'create product', :js do 6 | let(:basket) { create(:basket) } 7 | 8 | context "as user" do 9 | before do 10 | sign_in('john') 11 | visit basket_path(basket) 12 | end 13 | 14 | scenario 'creates product' do 15 | page.find("#add_product_btn").click 16 | 17 | within "#product_form" do 18 | fill_in 'Name', with: 'Test product' 19 | click_on 'Save' 20 | end 21 | 22 | expect(page).to have_content I18n.t('products.create.message') 23 | expect(page).to have_content 'Test product' 24 | end 25 | end 26 | 27 | context "multiple sessions" do 28 | scenario "all users see new product in real-time" do 29 | Capybara.using_session('first') do 30 | sign_in('john') 31 | visit basket_path(basket) 32 | end 33 | 34 | Capybara.using_session('second') do 35 | sign_in('jack') 36 | visit basket_path(basket) 37 | end 38 | 39 | Capybara.using_session('first') do 40 | page.find("#add_product_btn").click 41 | 42 | within "#product_form" do 43 | fill_in 'Name', with: 'Test product' 44 | click_on 'Save' 45 | end 46 | 47 | expect(page).to have_content I18n.t('products.create.message') 48 | expect(page).to have_content 'Test product' 49 | end 50 | 51 | Capybara.using_session('second') do 52 | expect(page).to have_content 'Test product' 53 | end 54 | end 55 | 56 | scenario "users on /baskets see basket count updates in real-time" do 57 | Capybara.using_session('first') do 58 | sign_in('john') 59 | visit basket_path(basket) 60 | end 61 | 62 | Capybara.using_session('second') do 63 | sign_in('jack') 64 | visit baskets_path 65 | end 66 | 67 | Capybara.using_session('second') do 68 | find("#basket_#{basket.id} .badge", text: 0, match: :prefer_exact) 69 | end 70 | 71 | Capybara.using_session('first') do 72 | page.find("#add_product_btn").click 73 | 74 | within "#product_form" do 75 | fill_in 'Name', with: 'Test product' 76 | click_on 'Save' 77 | end 78 | 79 | expect(page).to have_content I18n.t('products.create.message') 80 | expect(page).to have_content 'Test product' 81 | end 82 | 83 | Capybara.using_session('second') do 84 | find("#basket_#{basket.id} .badge", text: 1, match: :prefer_exact) 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/acceptance/disconnection_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'acceptance_helper' 4 | 5 | feature 'disconnection', :js do 6 | let(:basket) { create(:basket) } 7 | 8 | scenario "user disconnected notification" do 9 | sign_in('john') 10 | visit baskets_path 11 | expect(page).to have_content "Welcome, john!" 12 | 13 | Capybara.using_session(:b) do 14 | sign_in('jack') 15 | visit baskets_path 16 | expect(page).to have_content "Welcome, jack!" 17 | page.evaluate_script "App.disconnect();" 18 | end 19 | 20 | expect(page).to have_content "jack disconnected" 21 | end 22 | 23 | scenario "on product page" do 24 | sign_in('john') 25 | visit baskets_path 26 | expect(page).to have_content "Welcome, john!" 27 | 28 | Capybara.using_session(:b) do 29 | sign_in('jack') 30 | visit basket_path(basket) 31 | expect(page).to have_content "Welcome, jack!" 32 | page.evaluate_script "App.disconnect();" 33 | end 34 | 35 | expect(page).to have_content "jack disconnected" 36 | expect(page).to have_content "jack left this basket page" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/acceptance/notifications_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'acceptance_helper' 4 | 5 | feature 'notifications', :js do 6 | context "multiple sessions" do 7 | before do 8 | Capybara.using_session(:a) do 9 | sign_in('john') 10 | visit baskets_path 11 | end 12 | 13 | Capybara.using_session(:b) do 14 | sign_in('jack') 15 | visit baskets_path 16 | end 17 | end 18 | 19 | scenario "all users see notifications" do 20 | ActionCable.server.broadcast "notifications", type: 'alert', data: 'Slow Connection!' 21 | 22 | Capybara.using_session(:a) do 23 | expect(page).to have_content 'Slow Connection!' 24 | end 25 | 26 | ActionCable.server.broadcast "notifications", type: 'alert', data: 'Slow Connection!' 27 | 28 | Capybara.using_session(:b) do 29 | expect(page).to have_content 'Slow Connection!' 30 | end 31 | 32 | ActionCable.server.broadcast "notifications_john", type: 'success', data: 'Hi, John!' 33 | ActionCable.server.broadcast "notifications_jack", type: 'success', data: 'Hi, Jack!' 34 | 35 | Capybara.using_session(:a) do 36 | expect(page).to have_content 'Hi, John!' 37 | expect(page).not_to have_content 'Hi, Jack!' 38 | end 39 | 40 | Capybara.using_session(:b) do 41 | expect(page).to have_content 'Hi, Jack!' 42 | expect(page).not_to have_content 'Hi, John!' 43 | end 44 | end 45 | end 46 | 47 | scenario "unsubscribe from notifications and subscribe again" do 48 | sign_in('john') 49 | visit baskets_path 50 | 51 | expect(page).to have_content 'Welcome, john!' 52 | 53 | ActionCable.server.broadcast "notifications", type: 'notice', data: 'Bla-bla' 54 | expect(page).to have_content 'Bla-bla' 55 | 56 | page.find("#notifications_btn").click 57 | 58 | ActionCable.server.broadcast "notifications", type: 'notice', data: 'Are you here?' 59 | ActionCable.server.broadcast "notifications_john", type: 'success', data: 'Come back, John!' 60 | 61 | expect(page).not_to have_content 'Are you here?' 62 | expect(page).not_to have_content 'Come back, John!' 63 | 64 | # Ensure that the first notification disappeared 65 | expect(page).not_to have_content 'Welcome, john!' 66 | 67 | page.find("#notifications_btn").click 68 | expect(page).to have_content 'Welcome, john!' 69 | 70 | ActionCable.server.broadcast "notifications", type: 'success', data: 'Hi, John!' 71 | expect(page).to have_content 'Hi, John!' 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/acceptance_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "capybara/rspec" 5 | require "selenium-webdriver" 6 | require "rack_session_access/capybara" 7 | require "puma" 8 | 9 | require "bg_helper" if Nenv.cable_url? && !Nenv.skip_bg? 10 | 11 | RSpec.configure do |config| 12 | include ActionView::RecordIdentifier 13 | config.include AcceptanceHelper, type: :feature 14 | config.include ShowMeTheCookies, type: :feature 15 | 16 | config.include_context "feature", type: :feature 17 | 18 | Capybara.server = :puma, { Silent: true } 19 | Capybara.server_host = '0.0.0.0' 20 | Capybara.server_port = 3002 21 | Capybara.default_max_wait_time = 5 22 | Capybara.save_path = "./tmp/capybara_output" 23 | Capybara.always_include_port = true 24 | Capybara.raise_server_errors = true 25 | 26 | # See https://github.com/GoogleChrome/puppeteer/issues/1645#issuecomment-356060348 27 | CHROME_OPTIONS = %w[ 28 | --no-sandbox 29 | --disable-background-networking 30 | --disable-default-apps 31 | --disable-extensions 32 | --disable-sync 33 | --disable-gpu 34 | --disable-translate 35 | --headless 36 | --hide-scrollbars 37 | --metrics-recording-only 38 | --mute-audio 39 | --no-first-run 40 | --safebrowsing-disable-auto-update 41 | --ignore-certificate-errors 42 | --ignore-ssl-errors 43 | --ignore-certificate-errors-spki-list 44 | --user-data-dir=/tmp 45 | ].freeze 46 | 47 | Capybara.register_driver :selenium_chrome do |app| 48 | driver = 49 | Capybara::Selenium::Driver.new( 50 | app, 51 | browser: :chrome, 52 | options: Selenium::WebDriver::Chrome::Options.new( 53 | args: CHROME_OPTIONS 54 | ) 55 | ) 56 | 57 | driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(1024, 740) 58 | driver 59 | end 60 | 61 | Capybara.javascript_driver = Capybara.default_driver = :selenium_chrome 62 | 63 | config.append_after(:each) { Capybara.reset_sessions! } 64 | end 65 | -------------------------------------------------------------------------------- /spec/bg_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | hive_pid = nil 4 | 5 | procfile = ENV['PROCFILE'] || 'Procfile.spec' 6 | 7 | wait = Regexp.new(ENV['BG_WAIT'] || '.+') 8 | 9 | puts "Starting background processes..." 10 | rout, wout = IO.pipe 11 | hive_pid = Process.spawn("hivemind #{procfile}", out: wout) 12 | 13 | Timeout.timeout(10) do 14 | loop do 15 | output = rout.readline 16 | break if output =~ wait 17 | end 18 | end 19 | 20 | puts "Background processes have been started." 21 | 22 | at_exit do 23 | puts "Stopping background processes..." 24 | Process.kill 'INT', hive_pid 25 | Process.wait hive_pid 26 | puts "Background processes have been stopped." 27 | end 28 | -------------------------------------------------------------------------------- /spec/factories/baskets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :basket do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is copied to spec/ when you run 'rails generate rspec:install' 4 | ENV['RAILS_ENV'] ||= 'test' 5 | require File.expand_path('../config/environment', __dir__) 6 | # Prevent database truncation if the environment is production 7 | abort("The Rails environment is running in production mode!") if Rails.env.production? 8 | require 'spec_helper' 9 | require 'rspec/rails' 10 | require 'factory_bot_rails' 11 | require "faker/food" 12 | 13 | Shoulda::Matchers.configure do |config| 14 | config.integrate do |with| 15 | with.test_framework :rspec 16 | with.library :rails 17 | end 18 | end 19 | 20 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 21 | Dir[Rails.root.join("spec/shared_contexts/**/*.rb")].each { |f| require f } 22 | Dir[Rails.root.join("spec/shared_examples/**/*.rb")].each { |f| require f } 23 | 24 | ActiveRecord::Migration.maintain_test_schema! 25 | 26 | Rails.logger = ActionCable.server.config.logger = ActiveRecord::Base.logger = Logger.new(STDOUT) if Nenv.log? 27 | 28 | RSpec.configure do |config| 29 | config.include FactoryBot::Syntax::Methods 30 | 31 | config.before(:suite) do 32 | DatabaseCleaner.clean_with :truncation 33 | Zonebie.set_random_timezone 34 | end 35 | 36 | config.before(:each) { DatabaseCleaner.strategy = :transaction } 37 | 38 | config.before(:each, js: true) { DatabaseCleaner.strategy = :truncation } 39 | 40 | config.before(:each) { DatabaseCleaner.start } 41 | 42 | config.after(:each) do 43 | # Clear Rails cache 44 | Rails.cache.clear 45 | # Clean DB 46 | DatabaseCleaner.clean 47 | # Timecop 48 | Timecop.return 49 | end 50 | 51 | config.shared_context_metadata_behavior = :apply_to_host_groups 52 | 53 | config.infer_base_class_for_anonymous_controllers = false 54 | 55 | config.use_transactional_fixtures = false 56 | 57 | config.infer_spec_type_from_file_location! 58 | 59 | config.filter_rails_from_backtrace! 60 | config.filter_gems_from_backtrace 'active_model_serializers' 61 | end 62 | -------------------------------------------------------------------------------- /spec/shared_contexts/shared_bg.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context "background_processes" do 4 | before(:all) do 5 | rout, wout = IO.pipe 6 | @hive_pid = Process.spawn('hivemind Procfile.spec', out: wout) 7 | 8 | Timeout.timeout(10) do 9 | loop do 10 | output = rout.readline 11 | break if output =~ /RPC server is listening on/ 12 | end 13 | end 14 | end 15 | 16 | after(:all) do 17 | Process.kill 'SIGKILL', @hive_pid 18 | Process.wait @hive_pid 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/shared_contexts/shared_feature.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context "feature", type: :feature do 4 | after(:each) do |example| 5 | next unless example.exception 6 | 7 | meta = example.metadata 8 | next unless meta[:js] == true 9 | 10 | filename = File.basename(meta[:file_path]) 11 | line_number = meta[:line_number] 12 | screenshot_name = "screenshot-#{filename}-#{line_number}.png" 13 | save_screenshot(screenshot_name) # rubocop:disable Lint/Debugger 14 | puts meta[:full_description] + "\n Screenshot: #{Capybara.save_path}/#{screenshot_name}" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # The `.rspec` file also contains a few flags that are not defaults but that 18 | # users commonly want. 19 | # 20 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 21 | RSpec.configure do |config| 22 | # rspec-expectations config goes here. You can use an alternate 23 | # assertion/expectation library such as wrong or the stdlib/minitest 24 | # assertions if you prefer. 25 | config.expect_with :rspec do |expectations| 26 | # This option will default to `true` in RSpec 4. It makes the `description` 27 | # and `failure_message` of custom matchers include text for helper methods 28 | # defined using `chain`, e.g.: 29 | # be_bigger_than(2).and_smaller_than(4).description 30 | # # => "be bigger than 2 and smaller than 4" 31 | # ...rather than: 32 | # # => "be bigger than 2" 33 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 34 | end 35 | 36 | # rspec-mocks config goes here. You can use an alternate test double 37 | # library (such as bogus or mocha) by changing the `mock_with` option here. 38 | config.mock_with :rspec do |mocks| 39 | # Prevents you from mocking or stubbing a method that does not exist on 40 | # a real object. This is generally recommended, and will default to 41 | # `true` in RSpec 4. 42 | mocks.verify_partial_doubles = true 43 | end 44 | 45 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 46 | # have no way to turn it off -- the option exists only for backwards 47 | # compatibility in RSpec 3). It causes shared context metadata to be 48 | # inherited by the metadata hash of host groups and examples, rather than 49 | # triggering implicit auto-inclusion in groups with matching metadata. 50 | config.shared_context_metadata_behavior = :apply_to_host_groups 51 | 52 | # The settings below are suggested to provide a good initial experience 53 | # with RSpec, but feel free to customize to your heart's content. 54 | # This allows you to limit a spec run to individual examples or groups 55 | # you care about by tagging them with `:focus` metadata. When nothing 56 | # is tagged with `:focus`, all examples get run. RSpec also provides 57 | # aliases for `it`, `describe`, and `context` that include `:focus` 58 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 59 | config.filter_run_when_matching :focus 60 | 61 | # Allows RSpec to persist some state between runs in order to support 62 | # the `--only-failures` and `--next-failure` CLI options. We recommend 63 | # you configure your source control system to ignore this file. 64 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt" 65 | 66 | # Limits the available syntax to the non-monkey patched syntax that is 67 | # recommended. For more details, see: 68 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 69 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 70 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 71 | # config.disable_monkey_patching! 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = 'doc' 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | # config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | end 100 | -------------------------------------------------------------------------------- /spec/support/acceptance_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AcceptanceHelper 4 | def sign_in(name) 5 | page.set_rack_session(username: name) 6 | create_cookie(:username, name) 7 | end 8 | 9 | def save_screenshot(name = nil) 10 | path = name || "screenshot-#{Time.now.utc.iso8601.delete('-:')}.png" 11 | page.save_screenshot path 12 | end 13 | 14 | # Opens test server (using launchy), authorize as user (if provided) 15 | # and wait for specified time (in seconds) until continue spec execution 16 | # 17 | # If you specify 0 as 'wait' you should manually resume spec execution. 18 | def visit_server(_user: nil, wait: 2, path: '/') 19 | url = "http://192.168.60.101:#{Capybara.server_port}" 20 | url += path 21 | 22 | p "Visit server on: #{url}" 23 | 24 | if wait == 0 25 | p "Type any key to continue..." 26 | $stdin.gets 27 | p "Done." 28 | else 29 | sleep wait 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/javascripts/phantom.js: -------------------------------------------------------------------------------- 1 | if (typeof Function.prototype.bind != 'function') { 2 | Function.prototype.bind = function bind(obj) { 3 | var args = Array.prototype.slice.call(arguments, 1), 4 | self = this, 5 | nop = function() { 6 | }, 7 | bound = function() { 8 | return self.apply( 9 | this instanceof nop ? this : (obj || {}), args.concat( 10 | Array.prototype.slice.call(arguments) 11 | ) 12 | ); 13 | }; 14 | nop.prototype = this.prototype || {}; 15 | bound.prototype = new nop(); 16 | return bound; 17 | }; 18 | } 19 | 20 | //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill 21 | if (typeof Object.assign != 'function') { 22 | (function () { 23 | Object.assign = function (target) { 24 | 'use strict'; 25 | if (target === undefined || target === null) { 26 | throw new TypeError('Cannot convert undefined or null to object'); 27 | } 28 | 29 | var output = Object(target); 30 | for (var index = 1; index < arguments.length; index++) { 31 | var source = arguments[index]; 32 | if (source !== undefined && source !== null) { 33 | for (var nextKey in source) { 34 | if (source.hasOwnProperty(nextKey)) { 35 | output[nextKey] = source[nextKey]; 36 | } 37 | } 38 | } 39 | } 40 | return output; 41 | }; 42 | })(); 43 | } 44 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anycable/anycable_demo/20be66959fe60b31b597cecddddf2ef457ad45c9/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------