How to create a notification system with Sidekiq, Redis and Currency in Rails 6

0

How to create a notification system with Sidekiq, Redis and Currency in Rails 6: How to create a real-time notification system. The project is a simple blog with user authentication and the necessary facade to display our notifications. We will use Redis to store all of its work and operational data. We will save the user who logged in to a cookie (this will help us get it from other places, we will see an interesting solution to this problem with Currency using Warden Hooks.

picture
Profile picture of Matias Carpintini Hacker Noon

In this article, we are going to talk a lot about asynchronous functions, something very fashionable nowadays, but not so much a few years ago.

With that, I don’t mean to say it’s something necessarily new, but I believe that thanks to the JS ecosystem today, the world is happening in real time.

In this article, I want to teach the key concepts of this. But as always, we do not stay in theory and we will see a real implementation, like real-time notifications from our application.

I will try to be as brief and as direct as possible.

Definitions and concepts

Asynchronous programming: refers to the occurrence of events that occur in our program. They are executed independently of our main program and never interfere with their execution. Before that, we had to wait for a response to continue running, which seriously affected the user experience.

WebSockets: WebSockets represent a long-awaited evolution in client / server web technology. They provide a long-lasting, single TCP socket connection between client and server, allowing full duplex, two-way messages to be delivered instantly with little overhead resulting in a very low latency connection.

Click here to learn more about web sockets.

In other words, it allows us to establish a peer-to-peer connection between the client and the server. Before that, the client was only the one who knew where the server was located, not the other way around.

Thanks to this, we can send a request to the server, and continue to run our program, without waiting for your response. The server then knows where the client is and can send you the response.

picture

Let’s do it

All of the above already makes sense for our notification system, doesn’t it?

Before continuing, make sure Redis is installed. Sidekiq uses Redis to store all of its work and operational data.

?? If you don’t know what Redis is, you can find out on his official site.

Sidekiq helps us to work in the background in a super easy and efficient way. (Also, one of my favorite gems ♥ ️)

I created this project for this article in order to focus directly on what interests us. The project is a simple blog with user authentication and the necessary facade to display our notifications. You can download it and follow the article with me.

REMARK: you can see the full implementation in the “notifications” branch

Initialize the configuration …

In

config/routes.rb

we will climb the roads of ActionCable (framework for real-time communication on websockets)

Rails.application.routes.draw do
  # everything else...
  mount ActionCable.server => '/cable'
end

Now do you remember how WebSockets works? A peer-to-peer connection, in other words, is also a _channel_ (as we call it in Rails), and within that channel we still need to identify each user. This is so that the server can know who to respond to and who has made a request. In this case, we will identify it with the user.id (I am using currency)

So in

app/channels/application_cable/connection.rb

:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_user
    end

    def find_user
      user_id = cookies.signed["user.id"]
      current_user = User.find_by(id: user_id)

      if current_user
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

We will save the user who logged in to a cookie (this will help us get it from other places, we will see), an interesting solution for this (at least with Devise) is to use Warden Hooks

For this, we can create an initializer on our application,

config/initializers/warden_hooks.rb
Warden::Manager.after_set_user do |user, auth, opts|
    auth.cookies.signed["user.id"] = user.id
    auth.cookies.signed["user.expires_at"] = 30.minutes.from_now
  end

  Warden::Manager.before_logout do |user, auth, opts|
    auth.cookies.signed["user.id"] = nil
    auth.cookies.signed["user.expires_at"] = nil
  end

Now let’s create a table in our database to record every notification we create, for this,

$ rails g model Notification user:references item:references viewed:boolean

REMARK: :Object is a polymorphic association, I do it this way so that they can add different types of notifications)

Let’s clarify this and other details in our migration (

db/migrate/TIMESTAMP_create_notifications.rb

):

class CreateNotifications < ActiveRecord::Migration[6.0]
  def change
    create_table :notifications do |t|
      t.references :user, foreign_key: true
      t.references :item, polymorphic: true
      t.boolean :viewed, default: false

      t.timestamps
    end
  end
end

and,

$ rails db:migrate

In

app/models/notification.rb

we’re going to do a few things that we’ll see along the way.

class Notification < ApplicationRecord
  belongs_to :user
  belongs_to :item, polymorphic: true # Indicates a polymorphic reference

  after_create { NotificationBroadcastJob.perform_later(self) } # We make this later
  
  scope :leatest, ->{order("created_at DESC")}
  scope :unviewed, ->{where(viewed: false)} # This is like a shortcut

  # This returns the number of unviewed notifications
  def self.for_user(user_id)
    Notification.where(user_id: user_id).unviewed.count
  end
end

Let’s create a worry, remember that one of the most respected philosophies of Rails is DRY (Don’t Repeat Yourself), currently every notification requires the same to work (in templates) (again, in this project we only have posts, but we could have a lot of other things that we want to integrate into our notification system, so with this form it’s super simple).

For that,

app/models/concerns/notificable.rb
module Notificable
  extend ActiveSupport::Concern # module '::'

  included do # this appends in each place where we call this module
    has_many :notifications, as: :item
    after_commit :send_notifications_to_users
  end

  def send_notifications_to_users
    if self.respond_to? :user_ids # returns true if the model you are working with has a user_ids method
      NotificationSenderJob.perform_later(self)
    end
  end
end

Now we can include it in our

app/models/post.rb

. Remember that our

send_notifications_to_users

wait for the method

user_ids

to respond to you with the corresponding fix. Let’s do this (app / models / post.rb):

class Post < ApplicationRecord
  include Notificable
  belongs_to :user

  def user_ids
    User.all.ids # send the notification to that users
  end
end

We will create the job in charge of sending notifications, this is what we will send in the background and we will take care of it with Sidekiq. For that,

$ rails g job NotificationSender

Inside the work (

app/jobs/notification_sender_job.rb

):

class NotificationSenderJob < ApplicationJob
  queue_as :default

  def perform(item) # this method dispatch when job is called
    item.user_ids.each do |user_id|
      Notification.create(item: item, user_id: user_id)
    end
  end
end

Finally, we need to install Sidekiq (and Sinatra to make a few things easier).

Gemfile

:

# everything else...
gem 'sinatra', '~> 2.0', '>= 2.0.8.1'
gem 'sidekiq', '~> 6.0', '>= 6.0.7'

Do not forget,

$ bundle install

We will tell Rails that we will be using Sidekiq for tasks on the queue adapter (

config/application.rb

):

# everything else...
module Blog
  class Application < Rails::Application
    # everything else...
    config.active_job.queue_adapter = :sidekiq
  end
end

We are also going to set up the routes that Sidekiq provides us, among which some kind of backoffice for our background (later you can access it from localhost: 3000 / sidekiq), very interesting. In

config/routes.rb

:

require 'sidekiq/web'
Rails.application.routes.draw do
  # everything else...
  mount Sidekiq::Web => '/sidekiq'
end

We are now going to create the channel through which we will transmit our notifications.

$ rails g channel Notification

In the backend of this channel (

app/channels/notification_channel.rb

), we will subscribe users:

class NotificationChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications.#{current_user.id}" # in this way we identify to the user inside the channel later
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

And in the frontend of the chain (

app/javascript/channels/notification_channel.js

) it would be nice to send a push notification to the browser, there are many JS libraries that make this very easy (like this one), but in order not to burden the post, we will print a simple message to the console. So:

// everything else...
consumer.subscriptions.create("NotificationChannel", {
  // everything else...
  received(data) {
    if(data.action == "new_notification"){
      cosole.log(`New notification! Now you have ${data.message} unread notifications`) 
    } // we will define action & message in the next step
  }
});

At this point we already have a lot to do, let’s send this notification to the user! For that, we are going to create another job that does just that, remember, the previous job is in charge of creating the notifications, this one does the broadcast. So,

$ rails g job NotificationBroadcast

Inside

app/jobs/notification_broadcast_job.rb

:

class NotificationBroadcastJob < ApplicationJob
  queue_as :default

  def perform(notification)
    notification_count = Notification.for_user(notification.user_id)

    ActionCable.server.broadcast "notifications.#{ notification.user_id }", { action: "new_notification", message: notification_count }
  end
end

Fantastic, we already have everything working! ??

I’ll add a few things to the backend to complete the example.

First, I’m going to add a method to my user model so that I can count notifications that I haven’t seen yet. And the model is a good place to make this query. In

app/models/user.rb

:

class User < ApplicationRecord
  # everything else...
  def unviewed_notifications_count
    Notification.for_user(self.id)
  end
end

I will also create a controller,

$ rails g controller Notifications index

. Inside the controller (

app/controllers/notifications_controller.rb

) I will add some methods:

I am going to create a js view so that I can respond remotely and display the latest notifications in my navigation drop-down list. In

app/helpers/notifications_helper.rb

:

module NotificationsHelper
  def render_notifications(notifications)
    notifications.map do |notification|
      render partial: "notifications/#{notification.item_type.downcase}", locals:{notification: notification}
    end.join("").html_safe
  end
end

Add the link in your browser. In my case (

app/views/partials/notifications.html.erb

):

<%= link_to notifications_path, remote: true, data:{ type:"script" } %>

Don’t forget to add the paths (

app/config/routes.rb

) for this new controller.

# everything else...
Rails.application.routes.draw do
  # everything else...
  resources :notifications, only: [:index, :update]
end

Just create a partial for this element (like

app/views/notifications/_post.rb

).

They can include a link to “mark as seen”, like this:

<%= link_to notification_path(id: notification, notification:{viewed: true}), method: :put %>

To run it locally, you will need to run Redis (

$ redis-server

) and Sidekiq (

$ bundle exec sidekiq

) +

$ rails s

, open 3 terminal windows with these 3 commands executed in parallel.

That’s all, I hope you find this useful

Also posted on https://dev.to/matiascarpintini/real-time-notification-system-with-sidekiq-redis-and-devise-in-rails-6-33l9

Profile picture of Matias Carpintini Hacker Noon

Key words

Join Hacker Midi

Create your free account to unlock your personalized reading experience.


Source link

Leave A Reply

Your email address will not be published.