Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Showing progress of background jobs with Turbo

Showing progress of background jobs with Turbo

Presentation showing how to display real-time progress of background jobs with Hotwire Turbo.
Check out the repository

Michal L

April 10, 2024
Tweet

More Decks by Michal L

Other Decks in Programming

Transcript

  1. The app Two models: Joke belongs to JokeRequest Job fetching

    number of jokes from Chuck Norris API “Chuck Norris doesn't have a watch, HE decides what time it is.”
  2. 1. Broadcast from model class class Joke Joke < <

    ApplicationRecord ApplicationRecord after_create_commit after_create_commit -> ->( (joke joke) ) do do broadcast_replace_to broadcast_replace_to([ ([ joke joke. .jokes_request jokes_request, , "jokes_progress_bar" "jokes_progress_bar" ], ], target: target: "jokes_progress_bar" "jokes_progress_bar", , partial: partial: "jokes_requests/jokes_progress_bar" "jokes_requests/jokes_progress_bar", , locals: locals: { { jokes_request: jokes_request: joke joke. .jokes_request jokes_request } } ) ) end end belongs_to belongs_to :jokes_request :jokes_request end end
  3. # app/views/jokes_requests/_jokes_progress_bar.html.erb # app/views/jokes_requests/_jokes_progress_bar.html.erb <% <% progress_width progress_width = =

    jokes_request jokes_request. .jokes jokes. .count count / / jokes_request jokes_request. .amount amount. .to_f to_f * * 100 100 %> %> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> < <div div class class= ="h-0.5 bg-lime-500 rounded-full" "h-0.5 bg-lime-500 rounded-full" style style= ="width: <%= progress_width.to_i %>%;" "width: <%= progress_width.to_i %>%;"></ ></div div> > </ </div div> > 1. Broadcast from model # app/views/jokes_requests/show.html.erb # app/views/jokes_requests/show.html.erb <%= <%= turbo_stream_from turbo_stream_from @jokes_request @jokes_request, , "jokes_progress_bar" "jokes_progress_bar" %> %> # (..) # (..) <%= <%= render render "jokes_progress_bar" "jokes_progress_bar", , jokes_request jokes_request: : @jokes_request @jokes_request %> %>
  4. PROS Fast Easy CONS Callbacks Polluting model CRUD operations with

    broadcasts Not always applicable 1. Broadcast from model Chuck Norris can nail a hammer into a wall.
  5. 2. Broadcast from worker # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def

    update_progress_bar update_progress_bar( (number number) ) Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_replace_to broadcast_replace_to( ( [ [ jokes_request jokes_request, , "jokes_progress_bar" "jokes_progress_bar" ], ], target: target: "jokes_progress_bar" "jokes_progress_bar", , partial: partial: "jokes_requests/jokes_progress_bar" "jokes_requests/jokes_progress_bar", , locals: locals: { { actual: actual: number number, , limit: limit: jokes_request jokes_request. .amount amount } } ) ) end end
  6. 2. Broadcast from worker # app/views/jokes_requests/_jokes_progress_bar.html.erb # app/views/jokes_requests/_jokes_progress_bar.html.erb <% <%

    progress_width progress_width = = actual actual / / limit limit. .to_f to_f * * 100 100 %> %> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> < <div div class class= ="h-0.5 bg-lime-500 rounded-full" "h-0.5 bg-lime-500 rounded-full" style style= ="width: <%= progress_width.to_i %>%;" "width: <%= progress_width.to_i %>%;"></ ></div div> > </ </div div> > # app/views/jokes_requests/show.html.erb # app/views/jokes_requests/show.html.erb <%= <%= turbo_stream_from turbo_stream_from @jokes_request @jokes_request, , "jokes_progress_bar" "jokes_progress_bar" %> %> # (..) # (..) <%= <%= render render "jokes_progress_bar" "jokes_progress_bar", , actual actual: : @jokes_request @jokes_request. .jokes jokes. .count count, , limit limit: : @jokes_request @jokes_request. .amount amount %> %>
  7. PROS No callbacks Easy to test Broadcast limited to one

    source place CONS More work? Still needs to bind to some record/ unique user ID 2. Broadcast from worker
  8. 3. Pseudo-background job # app/views/jokes_request/show.html.erb # app/views/jokes_request/show.html.erb <% <% jokes_request

    jokes_request. .amount amount. .times times do do %> %> <%= turbo_frame_tag "joke", src: fetch_joke_path do %> <%= turbo_frame_tag "joke", src: fetch_joke_path do %> <% <% end end %> %> <% end %> <% end %>
  9. 3. Pseudo-background job # app/controllers/jokes_controller.rb # app/controllers/jokes_controller.rb def def fetch_joke

    fetch_joke @joke @joke = = FetchJokeService FetchJokeService. .new new. .call call end end # app/views/jokes/fetch_joke.html.erb # app/views/jokes/fetch_joke.html.erb <%= <%= turbo_frame_tag turbo_frame_tag "joke" "joke" do do %> %> <div class="bg-slate-200 text-slate-700 text-center text-clip overflow-clip py-4 mb-4 shadow-md <div class="bg-slate-200 text-slate-700 text-center text-clip overflow-clip py-4 mb-4 shadow-md rounded px-4"> rounded px-4"> <%= <%= @joke @joke %> %> </div> </div> <% <% end end %> %>
  10. Bonus: PAGINATION # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb class class FetchJokesService FetchJokesService

    # ... # ... def def add_joke add_joke( (joke joke, , jokes_count jokes_count) ) last_page last_page = = jokes_count jokes_count / / Joke::PER_PAGE Joke::PER_PAGE + + 1 1 if if jokes_count jokes_count % % Joke::PER_PAGE Joke::PER_PAGE == == 1 1 # change page to next one # change page to next one clear_all_jokes clear_all_jokes add_joke_card add_joke_card( (joke joke) ) replace_pagination replace_pagination( (jokes_count jokes_count, , last_page last_page) ) else else add_joke_card add_joke_card( (joke joke) ) end end end end end end
  11. Bonus: PAGINATION # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def clear_all_jokes clear_all_jokes

    Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_replace_to broadcast_replace_to( ( [ [ jokes_request jokes_request, , "jokes" "jokes" ], ], target: target: "jokes_grid" "jokes_grid", , partial: partial: "jokes_requests/empty_jokes_grid" "jokes_requests/empty_jokes_grid" ) ) end end # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def add_joke_card add_joke_card( (joke joke) ) Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_append_to broadcast_append_to( ( [ [ jokes_request jokes_request, , "jokes" "jokes" ], ], target: target: "jokes_grid" "jokes_grid", , partial: partial: "jokes/joke" "jokes/joke", , locals: locals: { { joke: joke: joke joke } } ) ) end end
  12. Bonus: PAGINATION # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def replace_pagination replace_pagination(

    (jokes_count jokes_count, , last_page last_page) ) pagy pagy = = Pagy Pagy. .new new( (count: count: jokes_count jokes_count, , page: page: last_page last_page, , items: items: Joke::PER_PAGE Joke::PER_PAGE, , link_extra: link_extra: 'data-turbo-action="advance"' 'data-turbo-action="advance"') ) Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_replace_to broadcast_replace_to( ( [ [ jokes_request jokes_request, , "jokes_pagination" "jokes_pagination" ], ], target: target: "jokes_pagination" "jokes_pagination", , partial: partial: "jokes_requests/jokes_pagination" "jokes_requests/jokes_pagination", , locals: locals: { { pagy: pagy: pagy pagy } } ) ) end end