Hotsock Turbo
Turbo Streams integration for Hotsock, enabling real-time updates in Rails applications using Hotsock's WebSocket infrastructure.
Overview
Hotsock Turbo provides a seamless way to broadcast Turbo Stream updates to your Rails application using Hotsock instead of Action Cable. It offers:
- Real-time page updates via WebSockets
- Model callbacks for automatic broadcasting on create, update, and destroy
- Support for Turbo Stream refresh (morphing) broadcasts
- Drop-in replacement mode for existing
Turbo::Broadcastableusage - Both synchronous and asynchronous (ActiveJob) broadcasting
Requirements
- Ruby >= 3.2
- Rails with Turbo
- Hotsock gem (>= 1.0)
- @hotsock/hotsock-js npm package
Installation
Add the gem to your Gemfile:
gem "turbo-rails"
gem "hotsock-turbo"Then run:
bundle installJavaScript Setup
Using Importmap
bin/importmap pin @hotsock/hotsock-jsThen in your application.js:
import "@hotsock/hotsock-js"
import "hotsock-turbo"Using npm/yarn
npm install @hotsock/hotsock-js
# or
yarn add @hotsock/hotsock-jsThen in your JavaScript entry point:
import "@hotsock/hotsock-js"
import "hotsock-turbo"Mount the Engine
Add to your config/routes.rb:
mount Hotsock::Turbo::Engine => "/hotsock"This provides a POST /hotsock/connect endpoint that returns connection tokens for the WebSocket client.
Configuration
Create an initializer at config/initializers/hotsock_turbo.rb:
Hotsock::Turbo.configure do |config|
# Required: Path to the engine's connect endpoint (or your own custom
# endpoint). This path must accept a POST request and return a JSON object
# with a `token` field that contains a connect-scoped JWT. {"token":"ey..."}
config.connect_token_path = "/hotsock/connect"
# Required: Your Hotsock WebSocket URL
config.wss_url = "wss://your-hotsock-instance.example.com"
# Optional: Log level for the JavaScript client (default: "warn")
config.log_level = "warn"
# Optional: Parent controller for any generated routes (default: "ApplicationController")
config.parent_controller = "ApplicationController"
# Optional: Enable drop-in replacement for Turbo::Broadcastable (default: false)
config.override_turbo_broadcastable = false
endConfiguration Options
| Option | Default | Description |
|---|---|---|
connect_token_path |
nil |
Path to your endpoint that issues Hotsock connection tokens |
wss_url |
nil |
Your Hotsock WebSocket server URL |
log_level |
"warn" |
JavaScript client log level ("debug", "info", "warn", "error") |
parent_controller |
"ApplicationController" |
Base controller for generated routes |
override_turbo_broadcastable |
false |
When true, overrides standard Turbo broadcast methods to use Hotsock |
suppress_broadcasts |
false |
When true, suppresses all Hotsock turbo broadcasts globally |
Usage
View Helpers
Meta Tags
Add the required meta tags to your layout (typically in <head>):
<%= hotsock_turbo_meta_tags %>This renders the configuration needed by the JavaScript client:
<meta name="hotsock:connect-token-path" content="/hotsock/connect_token" />
<meta name="hotsock:log-level" content="warn" />
<meta
name="hotsock:wss-url"
content="wss://your-hotsock-instance.example.com"
/>You can override configuration per-page:
<%= hotsock_turbo_meta_tags wss_url: "wss://other.example.com", log_level: "debug" %>Stream Subscriptions
Subscribe to streams in your views:
<%= hotsock_turbo_stream_from @board %>
<%= hotsock_turbo_stream_from @board, :messages %>
<%= hotsock_turbo_stream_from "custom_stream_name" %>This renders a custom element that manages the WebSocket subscription:
<hotsock-turbo-stream-source
data-channel="..."
data-token="..."
data-user-id="..."
>
</hotsock-turbo-stream-source>Model Broadcasting
Hotsock Turbo automatically includes broadcasting methods in all ActiveRecord models.
Broadcasting Refreshes
For models that should trigger page refreshes (works great with Turbo's morphing):
class Board < ApplicationRecord
# Broadcasts refresh to "boards" stream on create/update/destroy
hotsock_broadcasts_refreshes
end
class Column < ApplicationRecord
belongs_to :board
# Broadcasts refresh to the board's stream on create/update/destroy
hotsock_broadcasts_refreshes_to :board
endBroadcasting DOM Updates
For fine-grained DOM updates (append, replace, remove):
class Message < ApplicationRecord
belongs_to :board
# Broadcasts append on create, replace on update, remove on destroy
hotsock_broadcasts_to :board
end
class Comment < ApplicationRecord
# Broadcasts to "comments" stream by default
hotsock_broadcasts
endOptions
class Message < ApplicationRecord
belongs_to :board
# Customize the insert action and target
hotsock_broadcasts_to :board,
inserts_by: :prepend, # :append (default), :prepend
target: "board_messages", # DOM ID to target
partial: "messages/card" # Custom partial
endInstance Methods
Broadcast from anywhere in your application:
message = Message.find(1)
# Sync broadcasts (immediate)
message.hotsock_broadcast_refresh
message.hotsock_broadcast_refresh_to(board)
message.hotsock_broadcast_replace
message.hotsock_broadcast_replace_to(board)
message.hotsock_broadcast_append_to(board, target: "messages")
message.hotsock_broadcast_prepend_to(board, target: "messages")
message.hotsock_broadcast_remove
message.hotsock_broadcast_remove_to(board)
# Async broadcasts (via ActiveJob)
message.hotsock_broadcast_refresh_later
message.hotsock_broadcast_refresh_later_to(board)
message.hotsock_broadcast_replace_later
message.hotsock_broadcast_replace_later_to(board)
message.hotsock_broadcast_append_later_to(board, target: "messages")
message.hotsock_broadcast_prepend_later_to(board, target: "messages")Direct Channel Broadcasting
Broadcast without a model instance:
Hotsock::Turbo::StreamsChannel.broadcast_refresh_to(board)
Hotsock::Turbo::StreamsChannel.broadcast_append_to(
board,
target: "messages",
partial: "messages/message",
locals: { message: message }
)
Hotsock::Turbo::StreamsChannel.broadcast_remove_to(board, target: "message_123")Drop-in Turbo Replacement
If you have existing code using Turbo::Broadcastable methods, you can enable drop-in replacement mode:
# config/initializers/hotsock_turbo.rb
Hotsock::Turbo.configure do |config|
config.override_turbo_broadcastable = true
endNow standard Turbo method names work with Hotsock:
class Message < ApplicationRecord
belongs_to :board
# These now use Hotsock instead of Action Cable
broadcasts_refreshes_to :board
broadcasts_to :board
broadcasts
broadcasts_refreshes
end
# Instance methods also work
message.broadcast_refresh
message.broadcast_replace_later_to(board)Suppressing Broadcasts
Global Suppression (Test Environments)
Disable all broadcasts globally, useful for test environments where you don't want to require a Hotsock configuration:
# config/environments/test.rb
Hotsock::Turbo.config.suppress_broadcasts = trueBlock-based Suppression
Temporarily disable broadcasts within a block:
Message.suppressing_turbo_broadcasts do
# No broadcasts will be sent
Message.create!(content: "Silent message", board: board)
message.update!(content: "Silent update")
endCheck suppression status:
Message.suppressed_turbo_broadcasts? # => false
Message.suppressing_turbo_broadcasts do
Message.suppressed_turbo_broadcasts? # => true
endCustomization
Custom User Identification
By default, subscriptions use session.id as the user identifier. Override this by defining hotsock_uid in your helper or controller:
# app/helpers/application_helper.rb
module ApplicationHelper
private
def hotsock_uid
current_user&.id&.to_s || session.id.to_s
end
endHow It Works
- Meta tags provide configuration to the JavaScript client
-
<hotsock-turbo-stream-source>elements connect to Hotsock via WebSocket -
Model callbacks or direct calls trigger broadcasts via
Hotsock::Turbo::StreamsChannel - Hotsock delivers messages to subscribed clients
-
JavaScript client receives messages and calls
Turbo.renderStreamMessage()to update the DOM
License
This gem is available as open source under the terms of the MIT License.