Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Datastar Ruby SDK

Implement the Datastar SSE procotocol in Ruby. It can be used in any Rack handler, and Rails controllers.

Installation

Add this gem to your Gemfile

gem 'datastar'

Or point your Gemfile to the source

gem 'datastar', github: 'starfederation/datastar-ruby'

Usage

Initialize the Datastar dispatcher

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 envdatastar=Datastar.from_rack_env(env)

Sending updates to the browser

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.

One-off update:

datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))

In this mode, the response is closed after the fragment is sent.

Streaming updates

datastar.streamdo |sse|
sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))# Streaming multiple updates100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))endend

In this mode, the response is kept open until stream blocks have finished.

Concurrent streaming blocks

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 threadsdatastar.streamdo |sse|
100.timesdo |i|
sleep1sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))endenddatastar.streamdo |sse|
1000.timesdo |i|
sleep0.1sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))endend

See the examples directory.

Datastar methods

All these methods are available in both the one-off and the streaming modes.

patch_elements

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 objectsse.patch_elements(UserComponent.new)# Or pass optionssse.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>)])

remove_elements

Sugar on top of #patch_elements See https://data-star.dev/reference/sse_events#datastar-patch-elements

sse.remove_elements('#users')

patch_signals

See https://data-star.dev/reference/sse_events#datastar-patch-signals

sse.patch_signals(count: 4,user: {name: 'John'})

remove_signals

Sugar on top of #patch_signals

sse.remove_signals(['user.name','user.email'])

execute_script

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)

signals

See https://data-star.dev/guide/reactive_signals

Returns signals sent by the browser.

sse.signals# => { user: { name: 'John' } }

redirect

This is just a helper to send a script to update the browser's location.

sse.redirect('/new_location')

Lifecycle callbacks

on_connect

Register server-side code to run when the connection is first handled.

datastar.on_connectdoputs'A user has connected'end

on_client_disconnect

Register server-side code to run when the connection is closed by the client

datastar.on_client_disconnectdoputs'A user has disconnected connected'end

This callback's behaviour depends on the configured heartbeat

on_server_disconnect

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_disconnectdoputs'Server is done streaming'end

on_error

Ruby code to handle any exceptions raised by streaming blocks.

datastar.on_errordo |exception|
Sentry.notify(exception)end

Note that this callback can be configured globally, too.

heartbeat

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.streamdo |sse|
100.timesdo |i|
sleep1sse.merge_signalscount: iendend

You can also set it to a different number (in seconds)

heartbeat: 0.5

Per-stream override

The #stream method also accepts a heartbeat: keyword that overrides the constructor-level setting for a single call. This is useful when a dispatcher is generally configured with a heartbeat but a particular response doesn't need one (e.g. a one-shot update). The previous value is restored once the call returns.

datastar=Datastar.new(request:,response:)# default heartbeat# Disable heartbeat for this single responsedatastar.stream(heartbeat: false)do |sse|
sse.patch_elements(html)end

The one-shot helpers (#patch_elements, #remove_elements, #patch_signals, #remove_signals, #execute_script, #redirect) use this internally to avoid spawning a heartbeat thread for a single message.

Manual connection check

If 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.streamdo |sse|
# The event bus implementaton will check connection status when idle# by calling #check_connection! on itEventBus.subscribe('channel',sse)do |event|
sse.merge_signalseventName: event.nameendend

Global configuration

Datastar.configuredo |config|
# Global on_error callback# Can be overriden on specific instancesconfig.on_errordo |exception|
Sentry.notify(exception)end# Global heartbeat interval (or false, to disable)# Can be overriden on specific instancesconfig.heartbeat=0.3# Enable compression for SSE streams (default: false)# See the Compression section below for detailsconfig.compression=trueend

Compression

SSE 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.

Enabling compression

Per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: true)

Or globally:

Datastar.configuredo |config|
config.compression=trueend

When 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 vs gzip

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'

Configuration options

Datastar.configuredo |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] pairsconfig.compression=[[:br,{quality: 5}],:gzip]end

You can also set these per-instance:

datastar=Datastar.new(request:,response:,view_context:,compression: [:gzip]# only gzip, no brotli)# Or with per-encoder optionsdatastar=Datastar.new(request:,response:,view_context:,compression: [[:gzip,{level: 1}]])

Per-encoder options

Options are passed directly to the underlying compressor via the array form. Available options depend on the encoder.

Gzip (via Zlib::Deflate):

OptionDefaultDescription
:levelZlib::DEFAULT_COMPRESSIONCompression level (0-9). 0 = none, 1 = fastest, 9 = smallest. Zlib::BEST_SPEED and Zlib::BEST_COMPRESSION also work.
:mem_level8Memory usage (1-9). Higher uses more memory for better compression.
:strategyZlib::DEFAULT_STRATEGYAlgorithm strategy. Alternatives: Zlib::FILTERED, Zlib::HUFFMAN_ONLY, Zlib::RLE, Zlib::FIXED.

Brotli (via Brotli::Compressor, requires the brotli gem):

OptionDefaultDescription
:quality11Compression quality (0-11). Lower is faster, higher compresses better.
:lgwin22Base-2 log of sliding window size (10-24).
:lgblock0 (auto)Base-2 log of max input block size (16-24, or 0 for auto).
:mode:genericCompression mode: :generic, :text, or :font. :text is a good choice for SSE (UTF-8 HTML/JSON).

Proxy considerations

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.

Rendering Rails templates

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.streamdo |sse|
10.timesdo |i|
sleep1tpl=render_to_string('events/user',layout: false,locals: {name: "David #{i}"})sse.patch_elementstplendend

Rendering Phlex components

#patch_elements supports Phlex component instances.

sse.patch_elements(UserComponent.new(user: User.first))

Rendering ViewComponent instances

#patch_elements also works with ViewComponent instances.

sse.patch_elements(UserViewComponent.new(user: User.first))

Rendering #render_in(view_context) interfaces

Any object that supports the #render_in(view_context) => String API can be used as a fragment.

classMyComponentdefinitialize(name)@name=nameenddefrender_in(view_context)"<div>Hello #{@name}</div>""
endend
sse.patch_elementsMyComponent.new('Joe')

Tests

bundleexecrspec

Running Datastar's SDK test suite

Install dependencies.

bundle install

From this library's root, run the bundled-in test Rack app:

bundle puma -p 8000 examples/test.ru

From the main Datastar repo (you'll need Go installed)

cd sdk/tests
go run ./cmd/datastar-sdk-tests -server http://localhost:8000 

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.

About

Official Datastar Ruby SDK.

Resources

Stars

37 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages