Why is Turbo Stream not updating unless I add a delay?

837 Views Asked by At

Turbo is slow on JavaScript side that I am forced to delay Ruby side. If I don't delay the server response, there are no turbo stream updates on the page. My logs show every turbo stream is rendering and all the jobs get executed and transmitted.

Controller:

# app/controllers/grids_controller.rb
class GridsController < ApplicationController
  def play
    @job_id = PlayJob.perform_later(**@grid_data.attributes).job_id # Turbo broadcasts
    render :play, status: :created
  end
end

Turbo stream view:

<!-- app/views/grids/play.html.erb -->
<%= turbo_stream_from :play, @job_id %>
<div id="grid">
  <% unless Rails.env.test? %>
    <p><strong>Phase 0</strong></p>
    <div class="cells">
      <%= image_tag asset_url('loading.gif'), alt: 'loading', class: 'loading' %>
    </div>
  <% end %>
</div>

Turbo broadcasts:

# lib/grid.rb
class Grid
  def play(job_id)
    sleep 1 # I am forced to add delay of one second to get turbo to respond!

    loop do
      broadcast_to :play, job_id
      break if phase >= @phases

      sleep @phase_duration
      next_phase
    end
  end

  def broadcast_to(*streamable)
    Turbo::StreamsChannel.broadcast_update_to(
      streamable,
      target: 'grid',
      partial: 'grids/grid',
      locals: { grid: self }
    )
  end
end

Here is the code of all my app: https://github.com/chiaraani/life-game-web

1

There are 1 best solutions below

0
On BEST ANSWER

In your play action you're broadcasting to a channel that you haven't connected yet, then you're rendering play.html.erb where turbo_stream_from connects. By that time all the jobs have finished, which is why when you add delay there is time to connect and see updates.

You can also see it in the logs, all the jobs get done and then you see:

# jobs
# jobs
Turbo::StreamsChannel is streaming from ...

but there is nothing left to stream.

You have to have turbo_stream_from already connected before you submit the form and have #grid target already on the page ready to catch:

# app/views/new.html.erb

# NOTE: you can create Play model in `new` action and just do update.
#       this way you have an `id` to broadcast to. I just made a random one
#       in place.
<%= turbo_stream_from :play, (play_id = SecureRandom.uuid) %>

<%= form_with model: @grid_data, url: play_path, builder: GridFormBuilder do |f| %>
  # send `id` back to controller
  <%= hidden_field_tag :id, play_id %>

  <% GridData.attribute_names.each do |attribute| %>
    # just use `scope`
    <%= f.label attribute, t(attribute, scope: :questions) %> 
    <%= f.number_field attribute %>
  <% end %>

  <%= f.submit %>
<% end %>

<%= tag.div id: "grid" %>
# app/controllers/grids_controller.rb

def create
  @grid_data = GridData.new(**grid_params)

  respond_to do |format|
    if @grid_data.valid?
      # pass `id` so we can broadcast to it
      @grid_data.to_grid.play(params[:id])
      # it just needs to be an empty response, but it has to be `turbo_stream`
      format.turbo_stream { render inline: "" } 
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

You can remove all sleep delays and see how fast it is.