broadcast_refresh_to é uma escrita síncrona
Três arquivos desta aplicação chamam a mesma linha. Admin::Catalog::SaveCourseService, Admin::Catalog::ReorderCourseService e Admin::Catalog::ReorderLessonService terminam com Turbo::StreamsChannel.broadcast_refresh_to("catalog_roadmap"), e app/views/catalog/courses/index.html.erb abre com <%= turbo_stream_from "catalog_roadmap" %>. Todo visitante na página pública do roadmap recebe um refresh ao vivo no momento em que um admin edita um curso. Não há recarga de página, nem polling do navegador, nem Redis em lugar nenhum do Gemfile desta aplicação.
O curso de nível Pleno "Solid Queue: arquitetura de jobs", logo antes deste, seguiu um job por onze tabelas de fila apoiada em banco. Este curso segue essa chamada de broadcast pela terceira biblioteca "Solid": pub/sub apoiado em banco.
A chamada, uma camada de cada vez
broadcast_refresh_to é um método do próprio turbo-rails, definido em Turbo::Streams::Broadcasts:
def broadcast_refresh_to(*streamables, **attributes)
broadcast_stream_to(*streamables, content: turbo_stream_refresh_tag(**attributes))
end
def broadcast_stream_to(*streamables, content:)
streamables.flatten!
streamables.compact_blank!
if streamables.present?
ActionCable.server.broadcast stream_name_from(streamables), content
end
end
stream_name_from mapeia cada streamable e os junta com ":". Uma String simples não tem to_gid_param, então cai no .to_param, que numa String devolve ela mesma. O nome de canal que chega ao ActionCable.server.broadcast é a string literal "catalog_roadmap", sem assinatura, hash ou embrulho.
A assinatura acontece na outra ponta, quando turbo_stream_from gera o valor com que o navegador se inscreve (Turbo.signed_stream_verifier.generate). A camada de broadcast e de armazenamento no servidor lida com o nome de canal puro. O curso de nível Pleno "Turbo Streams: segurança", mais adiante nesta série, trata desse esquema de assinatura por inteiro. Este curso só precisa saber que o canal é uma string simples quando chega ao armazenamento.
content é a saída de turbo_stream_refresh_tag, e é minúscula. Aqui está uma linha lida direto do banco de cable de desenvolvimento desta aplicação:
SolidCable::Message.where(channel: "catalog_roadmap").first.payload
# => "\"\\u003cturbo-stream action=\\\"refresh\\\"\\u003e\\u003c/turbo-stream\\u003e\""
Esse payload guardado tem 70 bytes de JSON. Decodificado, é <turbo-stream action="refresh"></turbo-stream>, 45 caracteres. Nenhum dado de curso, nenhum fragmento de HTML e nenhum partial renderizado viaja pela rede. action="refresh" diz ao runtime do Turbo no navegador para buscar a página atual de novo, e os layouts desta aplicação definem turbo-refresh-method como morph, então o Turbo transforma o DOM no lugar. O broadcast só diz "algo mudou, vá olhar". O olhar é um GET HTTP comum que o navegador faz por conta própria.
Sem fila, sem classe de canal
ActionCable.server.broadcast é uma chamada de método simples, não perform_later. Ela roda na mesma thread que chamou broadcast_refresh_to, dentro da requisição do admin, antes de a resposta ser enviada.
Existe uma família _later_to (broadcast_refresh_later_to, broadcast_render_later_to) que passa pelo Active Job, via Turbo::Streams::BroadcastStreamJob. Mas um grep por _later_to no app/ desta aplicação volta vazio. Todo broadcast aqui é do tipo síncrono, então o Active Job e o Solid Queue não participam deste caminho. Mantenha essa fronteira nítida. É fácil supor que trabalho em segundo plano significa trabalho enfileirado, e aqui não significa.
Também não há nenhuma subclasse própria de ActionCable::Channel nesta aplicação: grep -rn "< ActionCable::Channel" app/ não devolve nada, e não existe diretório app/channels. turbo_stream_from e broadcast_refresh_to passam os dois pelo Turbo::StreamsChannel embutido do turbo-rails, que esta aplicação nunca estende nem configura. Escrever um canal próprio, com um método subscribed, autorização de stream customizada e ações que um cliente pode chamar no servidor, é terreno útil do Action Cable de que esta aplicação ainda não precisou. O curso de nível Pleno "ActionCable: canais personalizados", mais adiante nesta série, constrói um.
Onde o fio termina
Uma última peça explica por que isso funciona sem código de roteamento nenhum. O config/routes.rb não tem mount ActionCable.server, e ainda assim uma conexão WebSocket para /cable está ativa nesta aplicação. O próprio engine do Action Cable se monta:
# actioncable-8.1.3.1/lib/action_cable/engine.rb
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
# ...
unless config.action_cable.mount_path.nil?
app.routes.prepend do
mount ActionCable.server => config.action_cable.mount_path, internal: true, anchor: true
end
end
default_mount_path é "/cable". O engine põe o mount na frente das rotas no boot, o mesmo mecanismo que deixa qualquer engine do Rails acrescentar comportamento sem a aplicação escrever uma linha; o curso de nível Júnior "Visão geral de Rails engines" trata disso.
O que ainda não foi nomeado é qual adapter recebe essa chamada ao ActionCable.server.broadcast, e o que ele faz com um canal de string simples e um payload de 70 bytes. Isso é o config/cable.yml, e é onde a próxima aula começa.
Pontos-chave
- Três services (
SaveCourseService,ReorderCourseService,ReorderLessonService) chamamTurbo::StreamsChannel.broadcast_refresh_to("catalog_roadmap"), e a view pública do roadmap se inscreve comturbo_stream_from "catalog_roadmap". - O nome de canal que chega ao
ActionCable.server.broadcasté a string literal"catalog_roadmap". A assinatura pertence ao lado da inscrição, e a camada de broadcast e armazenamento nunca a toca. broadcast_refresh_toé síncrono: chamaActionCable.server.broadcastdiretamente, nãoperform_later. Esta aplicação nunca usa a família_later_to, então o Active Job e o Solid Queue não participam deste caminho.- Esta aplicação não tem subclasses próprias de
ActionCable::Channel. Todo broadcast e inscrição passa peloTurbo::StreamsChannelembutido do turbo-rails. - O Action Cable se monta sozinho em
/cablede dentro do próprio engine no boot; nada noconfig/routes.rbprecisa declará-lo.
Continue com a aula 2
Uma tabela, um canal com hash, e um insert cru
O Solid Cable guarda mensagens numa única tabela, converte canais em inteiros de 64 bits com sinal e grava com insert.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.