As Hitman Family we share the same contract, but we need realtime update of the contract list

That ‘s great because turbo comes with broadcasting partial page updates  delivered asynchronously over a web socket connection. Thanks to Action Cable turbo will provide easy tool to implement Real Time features.

In our case we will use Redis as for our cable implementation. ( using solid is even easier as it doesn’t require redis)

Redis Setup

Subscribe to a ActionCable channel

In our one page application, we want to modify our contract list, turbo comes with turbo_stream_from 'channel_name' method to subscribe to a channel

#app/views/index.html.erb
[...]
<div class="list-group">
    <%= turbo_frame_tag "new_contract" %>
    <%= turbo_stream_from "contracts" %> # Subscribe to "contracts" channel
    <% @contracts.each do |contract| %>
      <%= turbo_frame_tag "contracts" do%>
        <%= render 'contracts/contract', contract: contract %>
      <% end %>
    <% end %>
  </div>

After starting redis-server AND rails s, here a are the subscription logs when loading contract’s index

Started GET "/cable" for 127.0.0.1 at 2024-11-17 17:57:26 +0100
Started GET "/cable" [WebSocket] for 127.0.0.1 at 2024-11-17 17:57:26 +0100
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
Turbo::StreamsChannel is transmitting the subscription confirmation
Turbo::StreamsChannel is streaming from contracts

Broadcast From a model

To broadcast when an object is updated/created or destroyed, we add callbacks in the Contract model

class Contract < ApplicationRecord
  after_update_commit -> { broadcast_update_to "contracts", partial: "contracts/contract",
   locals: { contract: self }, target: "contracts" }
  after_create_commit -> { broadcast_prepend_to "contracts", partial: "contracts/contract",
   locals: { contract: self }, target: "contracts" }
  after_destroy_commit -> { broadcast_remove_to "contracts", target: "contract_#{id}"}
[...]
end

This code can be improve with private methods instead of lambda ( Not today)

broadcast_update_to "contracts", partial: "contracts/contract",
   locals: { contract: self }, target: "contracts"

Broadcast from a Controller or a Job

[...]
if @contract.update(contract_params)
      # Broadcast an update to the Turbo Streams channel
      @contract.broadcast_update_to(
        "contracts",
        partial: "contracts/contract",
        locals: { contract: @contract },
        target: "contract_#{@contract.id}"
      )
      render turbo_stream: turbo_stream.replace("contract_#{@contract.id}", partial: "contracts/contract", locals: { contract: @contract })
    else
 [...]
class ContractBroadcastJob < ApplicationJob
  queue_as :default

  def perform(contract_id)
    contract = Contract.find(contract_id)
    
    # Broadcast the update
    contract.broadcast_update_to(
      "contracts",
      partial: "contracts/contract",
      locals: { contract: contract },
      target: "contract_#{contract.id}"
    )
  end
end