Live timelines using ActionCable

This commit is contained in:
Eugen Rochko 2016-08-18 15:49:51 +02:00
parent 10ba09f546
commit 6deb9f966e
24 changed files with 99 additions and 53 deletions

View file

@ -35,7 +35,6 @@ gem 'onebox'
gem 'simple_form' gem 'simple_form'
gem 'will_paginate' gem 'will_paginate'
gem 'rack-attack' gem 'rack-attack'
gem 'turbolinks'
gem 'sidekiq' gem 'sidekiq'
gem 'sinatra', require: nil, github: 'sinatra' gem 'sinatra', require: nil, github: 'sinatra'
@ -66,5 +65,5 @@ group :production do
end end
group :development, :production do group :development, :production do
gem 'rack-mini-profiler', require: false gem 'rack-mini-profiler'
end end

View file

@ -321,9 +321,6 @@ GEM
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (2.0.5) tilt (2.0.5)
tool (0.2.3) tool (0.2.3)
turbolinks (5.0.1)
turbolinks-source (~> 5)
turbolinks-source (5.0.0)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.0.1) uglifier (3.0.1)
@ -394,7 +391,6 @@ DEPENDENCIES
simplecov simplecov
sinatra! sinatra!
therubyracer therubyracer
turbolinks
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
webmock webmock
will_paginate will_paginate

View file

@ -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/

View file

@ -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/

View file

@ -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/

View file

@ -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/

View file

@ -12,5 +12,4 @@
// //
//= require jquery //= require jquery
//= require jquery_ujs //= require jquery_ujs
//= require turbolinks
//= require_tree . //= require_tree .

View file

@ -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);

View file

@ -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));
}
});

View file

@ -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/

View file

@ -1,5 +0,0 @@
$ ->
$(document).on 'turbolinks:load', ->
unless typeof window.MiniProfiler == 'undefined'
window.MiniProfiler.init()
window.MiniProfiler.pageTransition()

View file

@ -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/

View file

@ -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/

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
# Profiling # Profiling
before_action do before_action do
if current_user && current_user.admin? if (current_user && current_user.admin?) || Rails.env == 'development'
Rack::MiniProfiler.authorize_request Rack::MiniProfiler.authorize_request
end end
end end

View file

@ -10,13 +10,13 @@ class FanOutOnWriteService < BaseService
private private
def deliver_to_self(status) def deliver_to_self(status)
push(:home, status.account.id, status) push(:home, status.account, status)
end end
def deliver_to_followers(status) def deliver_to_followers(status)
status.account.followers.each do |follower| status.account.followers.each do |follower|
next if !follower.local? || FeedManager.filter_status?(status, follower) next if !follower.local? || FeedManager.filter_status?(status, follower)
push(:home, follower.id, status) push(:home, follower, status)
end end
end end
@ -24,23 +24,38 @@ class FanOutOnWriteService < BaseService
status.mentions.each do |mention| status.mentions.each do |mention|
mentioned_account = mention.account mentioned_account = mention.account
next unless mentioned_account.local? next unless mentioned_account.local?
push(:mentions, mentioned_account.id, status) push(:mentions, mentioned_account, status)
end end
end end
def push(type, receiver_id, status) def push(type, receiver, status)
redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id)
trim(type, receiver_id) trim(type, receiver)
ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status))
end end
def trim(type, receiver_id) def trim(type, receiver)
return unless redis.zcard(FeedManager.key(type, receiver_id)) > FeedManager::MAX_ITEMS 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) 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}") redis.zremrangebyscore(FeedManager.key(type, receiver.id), '-inf', "(#{last.last}")
end end
def redis def redis
$redis $redis
end 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 end

View file

@ -8,7 +8,7 @@ class PrecomputeFeedService < BaseService
Status.send("as_#{type}_timeline", account).order('created_at desc').limit(FeedManager::MAX_ITEMS).each do |status| 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) 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 instant_return << status unless instant_return.size > limit
end end

View file

@ -1,5 +1,6 @@
development: development:
adapter: async adapter: redis
url: redis://localhost:6379/1
test: test:
adapter: async adapter: async

View file

@ -64,3 +64,6 @@ Rails.application.configure do
Bullet.rails_logger = true Bullet.rails_logger = true
end end
end end
require 'sidekiq/testing'
Sidekiq::Testing.inline!

View file

@ -8,4 +8,4 @@ Rails.application.config.assets.version = '1.0'
# Precompile additional assets. # Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # 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 )

View file

@ -1,6 +1,2 @@
require 'rack-mini-profiler' Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler)
Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater)
Rack::MiniProfilerRails.initialize!(Rails.application)
Rails.application.middleware.delete(Rack::MiniProfiler)
Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler)

View file

@ -1,6 +1,8 @@
require 'sidekiq/web' require 'sidekiq/web'
Rails.application.routes.draw do Rails.application.routes.draw do
mount ActionCable.server => '/cable'
authenticate :user, lambda { |u| u.admin? } do authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => '/sidekiq' mount Sidekiq::Web => '/sidekiq'
end end