diff --git a/README.md b/README.md index a3b86cb..9fd3bfe 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,21 @@ 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. + +```ruby +datastar = Datastar.new(request:, response:) # default heartbeat + +# Disable heartbeat for this single response +datastar.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. diff --git a/lib/datastar/dispatcher.rb b/lib/datastar/dispatcher.rb index 053ee96..691d1ca 100644 --- a/lib/datastar/dispatcher.rb +++ b/lib/datastar/dispatcher.rb @@ -28,7 +28,7 @@ class Dispatcher HTTP_ACCEPT = 'HTTP_ACCEPT' HTTP1 = 'HTTP/1.1' - attr_reader :request, :response + attr_reader :request, :response, :heartbeat # @option request [Rack::Request] the request object # @option response [Rack::Response, nil] the response object @@ -137,7 +137,7 @@ def signals # @param elements [String, #call(view_context: Object) => Object] the HTML elements or object # @param options [Hash] the options to send with the message def patch_elements(elements, options = BLANK_OPTIONS) - stream_no_heartbeat do |sse| + stream(heartbeat: false) do |sse| sse.patch_elements(elements, options) end end @@ -152,7 +152,7 @@ def patch_elements(elements, options = BLANK_OPTIONS) # @param selector [String] a CSS selector for the fragment to remove # @param options [Hash] the options to send with the message def remove_elements(selector, options = BLANK_OPTIONS) - stream_no_heartbeat do |sse| + stream(heartbeat: false) do |sse| sse.remove_elements(selector, options) end end @@ -166,7 +166,7 @@ def remove_elements(selector, options = BLANK_OPTIONS) # @param signals [Hash, String] signals to merge # @param options [Hash] the options to send with the message def patch_signals(signals, options = BLANK_OPTIONS) - stream_no_heartbeat do |sse| + stream(heartbeat: false) do |sse| sse.patch_signals(signals, options) end end @@ -180,7 +180,7 @@ def patch_signals(signals, options = BLANK_OPTIONS) # @param paths [Array] object paths to the signals to remove # @param options [Hash] the options to send with the message def remove_signals(paths, options = BLANK_OPTIONS) - stream_no_heartbeat do |sse| + stream(heartbeat: false) do |sse| sse.remove_signals(paths, options) end end @@ -194,7 +194,7 @@ def remove_signals(paths, options = BLANK_OPTIONS) # @param script [String] the script to execute # @param options [Hash] the options to send with the message def execute_script(script, options = BLANK_OPTIONS) - stream_no_heartbeat do |sse| + stream(heartbeat: false) do |sse| sse.execute_script(script, options) end end @@ -204,7 +204,7 @@ def execute_script(script, options = BLANK_OPTIONS) # # @param url [String] the URL or path to redirect to def redirect(url) - stream_no_heartbeat do |sse| + stream(heartbeat: false) do |sse| sse.redirect(url) end end @@ -245,10 +245,24 @@ def redirect(url) # By default, the built-in Rack finalzer just returns the resposne Array which can be used by any Rack handler. # On Rails, the Rails controller response is set to this objects streaming response. # + # A per-call +heartbeat:+ keyword overrides the constructor-level heartbeat + # for the duration of this call. Pass +false+ to disable heartbeat for a + # one-shot message (e.g. a single +patch_elements+), or a Numeric interval + # to enable it. The previous value is restored once the call returns. + # @example Disable heartbeat for a single response + # + # datastar.stream(heartbeat: false) do |sse| + # sse.patch_elements(html) + # end + # # @param streamer [#call(ServerSentEventGenerator), nil] a callable to call with the generator + # @param heartbeat [Numeric, false] override the heartbeat interval for this call, or +false+ to disable # @yieldparam sse [ServerSentEventGenerator] the generator object # @return [Object] depends on the finalize callback - def stream(streamer = nil, &block) + def stream(streamer = nil, heartbeat: @heartbeat, &block) + heartbeat_was = @heartbeat + @heartbeat = heartbeat + streamer ||= block @streamers << streamer if @heartbeat && !@heartbeat_on @@ -269,18 +283,12 @@ def stream(streamer = nil, &block) @response.body = body @finalize.call(@view_context, @response) + ensure + @heartbeat = heartbeat_was end private - def stream_no_heartbeat(&block) - was = @heartbeat - @heartbeat = false - stream(&block).tap do - @heartbeat = was - end - end - # Produce a response body for a single stream # In this case, the SSE generator can write directly to the socket # diff --git a/spec/dispatcher_spec.rb b/spec/dispatcher_spec.rb index 09b6157..549f856 100644 --- a/spec/dispatcher_spec.rb +++ b/spec/dispatcher_spec.rb @@ -498,6 +498,31 @@ def self.render_in(view_context) = %(
\n#{view_context}