visit
I will try to be as brief and straightforward as possible.
Asynchronous programming: refers to the occurrence of events that happen in our program. They are executed independently of our main program, and never interfere with their execution. Prior to this, we were forced to wait for a response to continue execution, seriously impairing the user experience.
WebSockets: WebSockets represent a long-awaited evolution in client/server web technology. They allow a long-held single TCP socket connection to be established between the client and server which allows for bi-directional, full duplex, messages to be instantly distributed with little overhead resulting in a very low latency connection.
.In other words, it allows us to establish a peer-to-peer connection between the client and the server. Before this, the client was only the one who knew where the server was, but not vice versa.Thanks to this, we can send a request to the server, and continue executing our program, without waiting for your response. Then the server knows where the client is, and can send you the response.👋 If you do not know what Redis is, you can discover it on its .
helps us to work in the background in a super simple and efficient way. (Also, one of my favorite gems ♥️)I created for this article in order to focus directly on what interests us. The project is a simple blog with user authentication and the necessary front to display our notifications. You can download it and follow the article with me.NOTE: you can see the full implementation in the "notifications" branch
Init configuration...
In
config/routes.rb
we will mount the routes of (framework for real-time communication over websockets)Rails.application.routes.draw do
# everything else...
mount ActionCable.server => '/cable'
end
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
For that, 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 save every notification we create, for this,
$ rails g model Notification user:references item:references viewed:boolean
NOTE: :item is a , I do it this way so they can add various types of notifications)
Let's specify 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 are going to do a couple of things that we will see on the go.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
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
expects the method user_ids
to reply to you with the respective fix. Let's do that (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 are going to create the in charge of sending the notifications, this is what we will send to the background and we will handle with Sidekiq. For that,
$ rails g job NotificationSender
Inside the job (
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
Gemfile
:# everything else...
gem 'sinatra', '~> 2.0', '>= 2.0.8.1'
gem 'sidekiq', '~> 6.0', '>= 6.0.7'
Don't forget,
$ bundle install
We are going to tell Rails that we will use Sidekiq for jobs 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 them, a kind of backoffice for our background (later you can have access 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
Now we are 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 channel (
app/javascript/channels/notification_channel.js
) it would be interesting to send a push notification to the browser, there are many JS libraries that make that very easy (like ), but in order not to make the post much heavier, we are going to print a simple message on 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 running, let's send that notification to the user! For this we are going to create another job that just does this, 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
First of all, I'm going to add a method to my user model so I can count the notifications I haven't seen yet. And the model is a good place to do this query. In
app/models/user.rb
:class User < ApplicationRecord
# everything else...
def unviewed_notifications_count
Notification.for_user(self.id)
end
end
I'm also going to create a controller,
$ rails g controller Notifications index
. Inside the controller (app/controllers/notifications_controller.rb
) i'm going to add some methods:I will create a js view to be able to respond remote and display the latest notifications in my dropdown in the nav. 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 nav. In my case (
app/views/partials/notifications.html.erb
):<%= link_to notifications_path, remote: true, data:{ type:"script" } %>
Let's not 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 item (like
app/views/notifications/_post.rb
). They can include a link to 'mark as seen', in this way:<%= link_to notification_path(id: notification, notification:{viewed: true}), method: :put %>
To run this locally you will have to run Redis (
$ redis-server
) and Sidekiq ($ bundle exec sidekiq
) + $ rails s
, have 3 terminal windows open with these 3 commands running in parallel.That is all, I hope it is useful to you 👋Also published at