Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.
Add this gem to your Gemfile
gem 'datastar'Or point your Gemfile to the source
gem 'datastar', github: 'starfederation/datastar-ruby'In your Rack handler or Rails controller:
# Rails controllers, as well as Sinatra and others,
# already have request and response objects.
# `view_context` is optional and is used to render Rails templates.
# Or view components that need access to helpers, routes, or any other context.
datastar = Datastar.new(request:, response:, view_context:)
# In a Rack handler, you can instantiate from the Rack env
datastar = Datastar.from_rack_env(env)There are two ways to use this gem in HTTP handlers:
- One-off responses, where you want to send a single update down to the browser.
- Streaming responses, where you want to send multiple updates down to the browser.
datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))In this mode, the response is closed after the fragment is sent.
datastar.stream do |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))
# Streaming multiple updates
100.times do |i|
sleep 1
sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))
end
endIn this mode, the response is kept open until stream blocks have finished.
Multiple stream blocks will be launched in threads/fibers, and will run concurrently.
Their updates are linearized and sent to the browser as they are produced.
# Stream to the browser from two concurrent threads
datastar.stream do |sse|
100.times do |i|
sleep 1
sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))
end
end
datastar.stream do |sse|
1000.times do |i|
sleep 0.1
sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))
end
endSee the examples directory.
All these methods are available in both the one-off and the streaming modes.
See https://data-star.dev/reference/sse_events#datastar-patch-elements
sse.patch_elements(%(<div id="foo">\n<span>hello</span>\n</div>))
# or a Phlex view object
sse.patch_elements(UserComponent.new)
# Or pass options
sse.patch_elements(
%(<div id="foo">\n<span>hello</span>\n</div>),
mode: 'append'
)You can patch multiple elements at once by passing an array of elements (or components):
sse.patch_elements([
%(<div id="foo">\n<span>hello</span>\n</div>),
%(<div id="bar">\n<span>world</span>\n</div>)
])Sugar on top of #patch_elements
See https://data-star.dev/reference/sse_events#datastar-patch-elements
sse.remove_elements('#users')See https://data-star.dev/reference/sse_events#datastar-patch-signals
sse.patch_signals(count: 4, user: { name: 'John' })Sugar on top of #patch_signals
sse.remove_signals(['user.name', 'user.email'])Sugar on top of #patch_elements. Appends a temporary <script> tag to the DOM, which will execute the script in the browser.
sse.execute_script(%(alert('Hello World!'))Pass attributes that will be added to the <script> tag:
sse.execute_script(%(alert('Hello World!')), attributes: { type: 'text/javascript' })These script tags are automatically removed after execution, so they can be used to run one-off scripts in the browser. Pass auto_remove: false if you want to keep the script tag in the DOM.
sse.execute_script(%(alert('Hello World!')), auto_remove: false)See https://data-star.dev/guide/reactive_signals
Returns signals sent by the browser.
sse.signals # => { user: { name: 'John' } }This is just a helper to send a script to update the browser's location.
sse.redirect('/new_location')Register server-side code to run when the connection is first handled.
datastar.on_connect do
puts 'A user has connected'
endRegister server-side code to run when the connection is closed by the client
datastar.on_client_disconnect do
puts 'A user has disconnected connected'
endThis callback's behaviour depends on the configured heartbeat
Register server-side code to run when the connection is closed by the server. Ie when the served is done streaming without errors.
datastar.on_server_disconnect do
puts 'Server is done streaming'
endRuby code to handle any exceptions raised by streaming blocks.
datastar.on_error do |exception|
Sentry.notify(exception)
endNote that this callback can be configured globally, too.
By default, streaming responses (using the #stream block) launch a background thread/fiber to periodically check the connection.
This is because the browser could have disconnected during a long-lived, idle connection (for example waiting on an event bus).
The default heartbeat is 3 seconds, and it will close the connection and trigger on_client_disconnect callbacks if the client has disconnected.
In cases where a streaming block doesn't need a heartbeat and you want to save precious threads (for example a regular ticker update, ie non-idle), you can disable the heartbeat:
datastar = Datastar.new(request:, response:, view_context:, heartbeat: false)
datastar.stream do |sse|
100.times do |i|
sleep 1
sse.merge_signals count: i
end
endYou can also set it to a different number (in seconds)
heartbeat: 0.5If you want to check connection status on your own, you can disable the heartbeat and use sse.check_connection!, which will close the connection and trigger callbacks if the client is disconnected.
datastar = Datastar.new(request:, response:, view_context:, heartbeat: false)
datastar.stream do |sse|
# The event bus implementaton will check connection status when idle
# by calling #check_connection! on it
EventBus.subscribe('channel', sse) do |event|
sse.merge_signals eventName: event.name
end
endDatastar.configure do |config|
# Global on_error callback
# Can be overriden on specific instances
config.on_error do |exception|
Sentry.notify(exception)
end
# Global heartbeat interval (or false, to disable)
# Can be overriden on specific instances
config.heartbeat = 0.3
# Enable compression for SSE streams (default: false)
# See the Compression section below for details
config.compression = true
endSSE data (JSON + HTML) is highly compressible, and long-lived connections benefit significantly from compression. This SDK supports opt-in Brotli and gzip compression for SSE streams.
Per-instance:
datastar = Datastar.new(request:, response:, view_context:, compression: true)Or globally:
Datastar.configure do |config|
config.compression = true
endWhen enabled, the SDK negotiates compression with the client via the Accept-Encoding header and sets the appropriate Content-Encoding response header. If the client does not support compression, responses are sent uncompressed.
Brotli (:br) is preferred by default as it offers better compression ratios. It requires the host app to require the brotli gem. Gzip uses Ruby built-in zlib and requires no extra dependencies.
To use Brotli, add the gem to your Gemfile:
gem 'brotli'Datastar.configure do |config|
# Enable compression (default: false)
# true enables both :br and :gzip (br preferred)
config.compression = true
# Or pass an array of encodings (first = preferred)
config.compression = [:br, :gzip]
# Per-encoder options via [symbol, options] pairs
config.compression = [[:br, { quality: 5 }], :gzip]
endYou can also set these per-instance:
datastar = Datastar.new(
request:, response:, view_context:,
compression: [:gzip] # only gzip, no brotli
)
# Or with per-encoder options
datastar = Datastar.new(
request:, response:, view_context:,
compression: [[:gzip, { level: 1 }]]
)Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.
Gzip (via Zlib::Deflate):
| Option | Default | Description |
|---|---|---|
:level |
Zlib::DEFAULT_COMPRESSION |
Compression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work. |
:mem_level |
8 |
Memory usage (1-9). Higher uses more memory for better compression. |
:strategy |
Zlib::DEFAULT_STRATEGY |
Algorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED. |
Brotli (via Brotli::Compressor, requires the brotli gem):
| Option | Default | Description |
|---|---|---|
:quality |
11 |
Compression quality (0-11). Lower is faster, higher compresses better. |
:lgwin |
22 |
Base-2 log of sliding window size (10-24). |
:lgblock |
0 (auto) |
Base-2 log of max input block size (16-24, or 0 for auto). |
:mode |
:generic |
Compression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON). |
Even with X-Accel-Buffering: no (set by default), some proxies like Nginx may buffer compressed responses. You may need to add proxy_buffering off to your Nginx configuration when using compression with SSE.
In Rails, make sure to initialize Datastar with the view_context in a controller.
This is so that rendered templates, components or views have access to helpers, routes, etc.
datastar = Datastar.new(request:, response:, view_context:)
datastar.stream do |sse|
10.times do |i|
sleep 1
tpl = render_to_string('events/user', layout: false, locals: { name: "David #{i}" })
sse.patch_elements tpl
end
end#patch_elements supports Phlex component instances.
sse.patch_elements(UserComponent.new(user: User.first))#patch_elements also works with ViewComponent instances.
sse.patch_elements(UserViewComponent.new(user: User.first))Any object that supports the #render_in(view_context) => String API can be used as a fragment.
class MyComponent
def initialize(name)
@name = name
end
def render_in(view_context)
"<div>Hello #{@name}</div>""
end
endsse.patch_elements MyComponent.new('Joe')bundle exec rspecInstall dependencies.
bundle installFrom this library's root, run the bundled-in test Rack app:
bundle puma -p 8000 examples/test.ruFrom the main Datastar repo (you'll need Go installed)
cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.