Hello, I was struggling a bit with completing chapter 11, iteration F4.
While the changes done in the “admin” (maintenance?) panel to store products showed up without me refreshing the main store page, it didn’t feel as smooth as it should have – the entire page refreshed, instead of just morphing the DOM to contain the new data. After spending a few hours investigating this, I found two root causes that should probably be addressed in the final book:
The entire rendering process is crashing due to not being able to render the product partial
This issue can be observed either from the terminal, where I could see this output (truncated):
02:38:35 web.1 | Started GET "/products/7" for ::1 at 2025-02-20 02:38:35 +0900
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 7 LIMIT 1 /*application='Depot'*/
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] Performing Turbo::Streams::ActionBroadcastJob (Job ID: 40cc8022-d8d7-4b62-9fcb-053676ba5413) from Async(default) enqueued at 2025-02-19T17:38:35.448256000Z with arguments: "products", {action: :replace, target: "product_7", targets: nil, attributes: {}, partial: "store/product", locals: {product: #<GlobalID:0x0000000138317188 @uri=#<URI::GID gid://depot/Product/7>>}}
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] ActiveStorage::Attachment Load (0.1ms) SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = 7 AND "active_storage_attachments"."record_type" = 'Product' AND "active_storage_attachments"."name" = 'image' LIMIT 1 /*application='Depot',job='Turbo%3A%3AStreams%3A%3AActionBroadcastJob'*/
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] ↳ app/views/store/_product.html.erb:3
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] ActiveStorage::Blob Load (0.0ms) SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = 7 LIMIT 1 /*application='Depot',job='Turbo%3A%3AStreams%3A%3AActionBroadcastJob'*/
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] ↳ app/views/store/_product.html.erb:3
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] Disk Storage (0.9ms) Generated URL for file at key: v7g25mgxk84oj9sxzjdpjyafgygx ()
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] Rendered store/_product.html.erb (Duration: 7.0ms | GC: 0.0ms)
02:38:35 web.1 | [ActiveJob] [Turbo::Streams::ActionBroadcastJob] [40cc8022-d8d7-4b62-9fcb-053676ba5413] Error performing Turbo::Streams::ActionBroadcastJob (Job ID: 40cc8022-d8d7-4b62-9fcb-053676ba5413) from Async(default) in 12.4ms: ActionView::Template::Error (Cannot generate URL for nrclient2.jpg using Disk service, please set ActiveStorage::Current.url_options.):
02:38:35 web.1 | /Users/milan/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/activestorage-8.0.1/lib/active_storage/service/disk_service.rb:138:in 'ActiveStorage::Service::DiskService#generate_url'
02:38:35 web.1 | /Users/milan/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/activestorage-8.0.1/lib/active_storage/service/disk_service.rb:117:in 'ActiveStorage::Service::DiskService#private_url'
02:38:35 web.1 | /Users/milan/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/activestorage-8.0.1/lib/active_storage/service.rb:125:in 'block in ActiveStorage::Service#url'
02:38:35 web.1 | /Users/milan/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/activesupport-8.0.1/lib/active_support/notifications.rb:210:in 'block in ActiveSupport::Notifications.instrument'
02:38:35 web.1 | /Users/milan/.local/share/mise/installs/ruby/3.4.1/lib/ruby/gems/3.4.0/gems/activesupport-8.0.1/lib/active_support/notifications/instrumenter.rb:58:in 'ActiveSupport::Notifications::Instrumenter#instrument'
Or in the browser’s developer console, where you can see the store main page unsubscribing from the WebSocket topic and resubscribing back to it – which should not happen. Also, the entire page just very obviously jumps, as it would if there was a full page reload happening.
Based on the error message, I thought that something akin to include ActiveStorage::SetCurrent
that we previously added to ApplicationController
also needs to be added to ApplicationCable::Channel
but that doesn’t seem to be the case, as the solution is much simpler – the store/_product
partial needs to be changed instead:
- <%= image_tag(product.image.url, class: "object-contain w-40 h-48 shadow mr-6") %>
+ <%= image_tag(product.image, class: "object-contain w-40 h-48 shadow mr-6") %>
After this, the crash no longer happens in the server console, but the page still hard-refreshes. That’s because…
The WebSockets topics overlap with chapter 6
Chapter 6 adds after_commit -> { broadcast_refresh_later_to "products" }
, introducing the products
topic, and then chapter 11 reuses the topic to broadcast not the information to refresh the page, but to replace the affected part. I think these should not share the same topic, so after changing ProductsChannel
like so:
- stream_from "products"
+ stream_from "store/products"
… ProductsController
like so:
- @product.broadcast_replace_later_to "products", partial: "store/product"
+ @product.broadcast_replace_later_to "store/products", partial: "store/product"
… and store’s view template like so:
- <%= turbo_stream_from "products" %>
+ <%= turbo_stream_from "store/products" %>
… everything starts working smoothly again, no full-page refreshes happen, and only the affected product gets replaced.
That was a fun one to figure out, as a Rails newbie!
(Also I feel like earlier in the chapter, the customer said to please honour the price of the product at the time of adding the product to the cart and I don’t think that’s happening yet but maybe it will be addressed later in the chapter…? Or maybe just not written yet, should probably involve putting the I missed that this is part of the optional exercises on pages 144~145. My bad!price
on LineItem
and copying the value over at the time of adding it to the cart.)