commit: 6deb9f966eb9a280cc16428ba9324ffc15ea60a8
parent: 10ba09f5466fe3d34a5ed78202c35f4e4f9d30e6
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 18 Aug 2016 15:49:51 +0200
Live timelines using ActionCable
Diffstat:
24 files changed, 99 insertions(+), 53 deletions(-)
diff --git a/Gemfile b/Gemfile
@@ -35,7 +35,6 @@ gem 'onebox'
gem 'simple_form'
gem 'will_paginate'
gem 'rack-attack'
-gem 'turbolinks'
gem 'sidekiq'
gem 'sinatra', require: nil, github: 'sinatra'
@@ -66,5 +65,5 @@ group :production do
end
group :development, :production do
- gem 'rack-mini-profiler', require: false
+ gem 'rack-mini-profiler'
end
diff --git a/Gemfile.lock b/Gemfile.lock
@@ -321,9 +321,6 @@ GEM
thread_safe (0.3.5)
tilt (2.0.5)
tool (0.2.3)
- turbolinks (5.0.1)
- turbolinks-source (~> 5)
- turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (3.0.1)
@@ -394,7 +391,6 @@ DEPENDENCIES
simplecov
sinatra!
therubyracer
- turbolinks
uglifier (>= 1.3.0)
webmock
will_paginate
diff --git a/app/assets/javascripts/api/accounts.coffee b/app/assets/javascripts/api/accounts.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/api/accounts/lookup.coffee b/app/assets/javascripts/api/accounts/lookup.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/api/follows.coffee b/app/assets/javascripts/api/follows.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/api/statuses.coffee b/app/assets/javascripts/api/statuses.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
@@ -12,5 +12,4 @@
//
//= require jquery
//= require jquery_ujs
-//= require turbolinks
//= require_tree .
diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js
@@ -0,0 +1,13 @@
+// Action Cable provides the framework to deal with WebSockets in Rails.
+// You can generate new channels where WebSocket features live using the rails generate channel command.
+//
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer();
+
+}).call(this);
diff --git a/app/assets/javascripts/channels/timeline.js b/app/assets/javascripts/channels/timeline.js
@@ -0,0 +1,13 @@
+App.timeline = App.cable.subscriptions.create("TimelineChannel", {
+ connected: function() {
+ console.log('Connected');
+ },
+
+ disconnected: function() {
+ console.log('Disconnected');
+ },
+
+ received: function(data) {
+ console.log(JSON.parse(data.message));
+ }
+});
diff --git a/app/assets/javascripts/oauth/applications.coffee b/app/assets/javascripts/oauth/applications.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/profiler.coffee b/app/assets/javascripts/profiler.coffee
@@ -1,5 +0,0 @@
-$ ->
- $(document).on 'turbolinks:load', ->
- unless typeof window.MiniProfiler == 'undefined'
- window.MiniProfiler.init()
- window.MiniProfiler.pageTransition()
diff --git a/app/assets/javascripts/settings.coffee b/app/assets/javascripts/settings.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/statuses.coffee b/app/assets/javascripts/statuses.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
@@ -0,0 +1,20 @@
+# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user
+
+ def connect
+ self.current_user = find_verified_user
+ end
+
+ protected
+
+ def find_verified_user
+ if verified_user = env['warden'].user
+ verified_user
+ else
+ reject_unauthorized_connection
+ end
+ end
+ end
+end
diff --git a/app/channels/timeline_channel.rb b/app/channels/timeline_channel.rb
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
+class TimelineChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "timeline:#{current_user.id}"
+ end
+
+ def unsubscribed
+ # Any cleanup needed when channel is unsubscribed
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
@@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
# Profiling
before_action do
- if current_user && current_user.admin?
+ if (current_user && current_user.admin?) || Rails.env == 'development'
Rack::MiniProfiler.authorize_request
end
end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
@@ -10,13 +10,13 @@ class FanOutOnWriteService < BaseService
private
def deliver_to_self(status)
- push(:home, status.account.id, status)
+ push(:home, status.account, status)
end
def deliver_to_followers(status)
status.account.followers.each do |follower|
next if !follower.local? || FeedManager.filter_status?(status, follower)
- push(:home, follower.id, status)
+ push(:home, follower, status)
end
end
@@ -24,23 +24,38 @@ class FanOutOnWriteService < BaseService
status.mentions.each do |mention|
mentioned_account = mention.account
next unless mentioned_account.local?
- push(:mentions, mentioned_account.id, status)
+ push(:mentions, mentioned_account, status)
end
end
- def push(type, receiver_id, status)
- redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id)
- trim(type, receiver_id)
+ def push(type, receiver, status)
+ redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id)
+ trim(type, receiver)
+ ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status))
end
- def trim(type, receiver_id)
- return unless redis.zcard(FeedManager.key(type, receiver_id)) > FeedManager::MAX_ITEMS
+ def trim(type, receiver)
+ return unless redis.zcard(FeedManager.key(type, receiver.id)) > FeedManager::MAX_ITEMS
- last = redis.zrevrange(FeedManager.key(type, receiver_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1)
- redis.zremrangebyscore(FeedManager.key(type, receiver_id), '-inf', "(#{last.last}")
+ last = redis.zrevrange(FeedManager.key(type, receiver.id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1)
+ redis.zremrangebyscore(FeedManager.key(type, receiver.id), '-inf', "(#{last.last}")
end
def redis
$redis
end
+
+ def inline_render(receiver, status)
+ rabl_scope = Class.new(BaseService) do
+ def initialize(account)
+ @account = account
+ end
+
+ def current_user
+ @account.user
+ end
+ end
+
+ Rabl::Renderer.new('api/statuses/show', status, view_path: 'app/views', format: :json, scope: rabl_scope.new(receiver)).render
+ end
end
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
@@ -8,7 +8,7 @@ class PrecomputeFeedService < BaseService
Status.send("as_#{type}_timeline", account).order('created_at desc').limit(FeedManager::MAX_ITEMS).each do |status|
next if type == :home && FeedManager.filter_status?(status, account)
- redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id)
+ redis.zadd(FeedManager.key(type, account.id), status.id, status.id)
instant_return << status unless instant_return.size > limit
end
diff --git a/config/cable.yml b/config/cable.yml
@@ -1,5 +1,6 @@
development:
- adapter: async
+ adapter: redis
+ url: redis://localhost:6379/1
test:
adapter: async
diff --git a/config/environments/development.rb b/config/environments/development.rb
@@ -64,3 +64,6 @@ Rails.application.configure do
Bullet.rails_logger = true
end
end
+
+require 'sidekiq/testing'
+Sidekiq::Testing.inline!
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
@@ -8,4 +8,4 @@ Rails.application.config.assets.version = '1.0'
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
-# Rails.application.config.assets.precompile += %w( search.js )
+Rails.application.config.assets.precompile += %w( cable.js )
diff --git a/config/initializers/rack-mini-profiler.rb b/config/initializers/rack-mini-profiler.rb
@@ -1,6 +1,2 @@
-require 'rack-mini-profiler'
-
-Rack::MiniProfilerRails.initialize!(Rails.application)
-
-Rails.application.middleware.delete(Rack::MiniProfiler)
-Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler)
+Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler)
+Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater)
diff --git a/config/routes.rb b/config/routes.rb
@@ -1,6 +1,8 @@
require 'sidekiq/web'
Rails.application.routes.draw do
+ mount ActionCable.server => '/cable'
+
authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end