Class: Datastar::Dispatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/datastar/dispatcher.rb

Overview

The Dispatcher encapsulates the logic of handling a request and building a response with streaming datastar messages. You’ll normally instantiate a Dispatcher in your controller action of Rack handler via Datastar.new.

Examples:


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

# One-off fragment response
datastar.patch_elements(template)

# Streaming response with multiple messages
datastar.stream do |sse|
  sse.patch_elements(template)
  10.times do |i|
    sleep 0.1
    sse.patch_signals(count: i)
  end
end

Constant Summary collapse

BLANK_BODY =
[].freeze
SSE_CONTENT_TYPE =
'text/event-stream'
SSE_ACCEPT_EXP =
/text\/event-stream/
HTTP_ACCEPT =
'HTTP_ACCEPT'
HTTP1 =
'HTTP/1.1'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request:, response: nil, view_context: nil, executor: Datastar.config.executor, error_callback: Datastar.config.error_callback, finalize: Datastar.config.finalize, heartbeat: Datastar.config.heartbeat, compression: Datastar.config.compression) ⇒ Dispatcher

Returns a new instance of Dispatcher.

Parameters:

  • request (Hash)

    a customizable set of options

  • response (Hash) (defaults to: nil)

    a customizable set of options

  • view_context (Hash) (defaults to: nil)

    a customizable set of options

  • executor (Hash) (defaults to: Datastar.config.executor)

    a customizable set of options

  • error_callback (Hash) (defaults to: Datastar.config.error_callback)

    a customizable set of options

  • finalize (Hash) (defaults to: Datastar.config.finalize)

    a customizable set of options

  • heartbeat (Hash) (defaults to: Datastar.config.heartbeat)

    a customizable set of options

Options Hash (request:):

  • the (Rack::Request)

    request object

Options Hash (response:):

  • the (Rack::Response, nil)

    response object

Options Hash (view_context:):

  • the (Object, nil)

    view context object, to use when rendering templates. Ie. a controller, or Sinatra app.

Options Hash (executor:):

  • the (Object)

    executor object to use for managing threads and queues

Options Hash (error_callback:):

  • the (Proc)

    callback to call when an error occurs

Options Hash (finalize:):

  • the (Proc)

    callback to call when the response is finalized

Options Hash (heartbeat:):

  • the (Integer, nil, FalseClass)

    heartbeat interval in seconds

Raises:

  • (ArgumentError)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/datastar/dispatcher.rb', line 40

def initialize(
  request:,
  response: nil,
  view_context: nil,
  executor: Datastar.config.executor,
  error_callback: Datastar.config.error_callback,
  finalize: Datastar.config.finalize,
  heartbeat: Datastar.config.heartbeat,
  compression: Datastar.config.compression
)
  @on_connect = []
  @on_client_disconnect = []
  @on_server_disconnect = []
  @on_error = [error_callback]
  @finalize = finalize
  @streamers = []
  @queue = nil
  @executor = executor
  @view_context = view_context
  @request = request
  @response = Rack::Response.new(BLANK_BODY, 200, response&.headers || {})
  @response.content_type = SSE_CONTENT_TYPE
  @response.headers['Cache-Control'] = 'no-cache'
  @response.headers['Connection'] = 'keep-alive' if @request.env['SERVER_PROTOCOL'] == HTTP1
  # Disable response buffering in NGinx and other proxies
  @response.headers['X-Accel-Buffering'] = 'no'
  @response.delete_header 'Content-Length'
  @executor.prepare(@response)
  raise ArgumentError, ':heartbeat must be a number' if heartbeat && !heartbeat.is_a?(Numeric)

  @heartbeat = heartbeat
  @heartbeat_on = false

  # Negotiate compression
  compression = CompressionConfig.build(compression) unless compression.is_a?(CompressionConfig)
  @compressor = compression.negotiate(request)
  @compressor.prepare_response(@response)
end

Instance Attribute Details

#heartbeatObject (readonly)

Returns the value of attribute heartbeat.



31
32
33
# File 'lib/datastar/dispatcher.rb', line 31

def heartbeat
  @heartbeat
end

#requestObject (readonly)

Returns the value of attribute request.



31
32
33
# File 'lib/datastar/dispatcher.rb', line 31

def request
  @request
end

#responseObject (readonly)

Returns the value of attribute response.



31
32
33
# File 'lib/datastar/dispatcher.rb', line 31

def response
  @response
end

Instance Method Details

#execute_script(script, options = BLANK_OPTIONS) ⇒ Object

One-off execute script in the UI See data-star.dev/reference/sse_events#datastar-execute-script

Examples:


datastar.execute_scriprt(%(alert('Hello World!'))

Parameters:

  • script (String)

    the script to execute

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



196
197
198
199
200
# File 'lib/datastar/dispatcher.rb', line 196

def execute_script(script, options = BLANK_OPTIONS)
  stream(heartbeat: false) do |sse|
    sse.execute_script(script, options)
  end
end

#on_client_disconnect(callable = nil, &block) ⇒ self

Register a callback for client disconnection Ex. when the browser is closed mid-stream

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Returns:

  • (self)


99
100
101
102
# File 'lib/datastar/dispatcher.rb', line 99

def on_client_disconnect(callable = nil, &block)
  @on_client_disconnect << (callable || block)
  self
end

#on_connect(callable = nil) {|sse| ... } ⇒ self

Register an on-connect callback Triggered when the request is handled

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Yield Parameters:

Returns:

  • (self)


90
91
92
93
# File 'lib/datastar/dispatcher.rb', line 90

def on_connect(callable = nil, &block)
  @on_connect << (callable || block)
  self
end

#on_error(callable = nil, &block) ⇒ self

Register a callback server-side exceptions Ex. when one of the server threads raises an exception

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Returns:

  • (self)


117
118
119
120
# File 'lib/datastar/dispatcher.rb', line 117

def on_error(callable = nil, &block)
  @on_error << (callable || block)
  self
end

#on_server_disconnect(callable = nil, &block) ⇒ self

Register a callback for server disconnection Ex. when the server finishes serving the request

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Returns:

  • (self)


108
109
110
111
# File 'lib/datastar/dispatcher.rb', line 108

def on_server_disconnect(callable = nil, &block)
  @on_server_disconnect << (callable || block)
  self
end

#patch_elements(elements, options = BLANK_OPTIONS) ⇒ Object

Examples:


datastar.patch_elements(%(<div id="foo">\n<span>hello</span>\n</div>\n))
# or a Phlex view object
datastar.patch_elements(UserComponet.new)

Parameters:

  • elements (String, #call(view_context: Object) => Object)

    the HTML elements or object

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



139
140
141
142
143
# File 'lib/datastar/dispatcher.rb', line 139

def patch_elements(elements, options = BLANK_OPTIONS)
  stream(heartbeat: false) do |sse|
    sse.patch_elements(elements, options)
  end
end

#patch_signals(signals, options = BLANK_OPTIONS) ⇒ Object

One-off patch signals in the UI See data-star.dev/reference/sse_events#datastar-patch-signals

Examples:


datastar.patch_signals(count: 1, toggle: true)

Parameters:

  • signals (Hash, String)

    signals to merge

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



168
169
170
171
172
# File 'lib/datastar/dispatcher.rb', line 168

def patch_signals(signals, options = BLANK_OPTIONS)
  stream(heartbeat: false) do |sse|
    sse.patch_signals(signals, options)
  end
end

#redirect(url) ⇒ Object

Send an execute_script event to change window.location

Parameters:

  • url (String)

    the URL or path to redirect to



206
207
208
209
210
# File 'lib/datastar/dispatcher.rb', line 206

def redirect(url)
  stream(heartbeat: false) do |sse|
    sse.redirect(url)
  end
end

#remove_elements(selector, options = BLANK_OPTIONS) ⇒ Object

One-off remove elements from the UI Sugar on top of patch-elements with mode: ‘remove’ See data-star.dev/reference/sse_events#datastar-patch-elements

Examples:


datastar.remove_elements('#users')

Parameters:

  • selector (String)

    a CSS selector for the fragment to remove

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



154
155
156
157
158
# File 'lib/datastar/dispatcher.rb', line 154

def remove_elements(selector, options = BLANK_OPTIONS)
  stream(heartbeat: false) do |sse|
    sse.remove_elements(selector, options)
  end
end

#remove_signals(paths, options = BLANK_OPTIONS) ⇒ Object

One-off remove signals from the UI See data-star.dev/reference/sse_events#datastar-remove-signals

Examples:


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

Parameters:

  • paths (Array<String>)

    object paths to the signals to remove

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



182
183
184
185
186
# File 'lib/datastar/dispatcher.rb', line 182

def remove_signals(paths, options = BLANK_OPTIONS)
  stream(heartbeat: false) do |sse|
    sse.remove_signals(paths, options)
  end
end

#signalsHash

Parse and returns Datastar signals sent by the client. See data-star.dev/guide/getting_started#data-signals

Returns:

  • (Hash)


125
126
127
# File 'lib/datastar/dispatcher.rb', line 125

def signals
  @signals ||= parse_signals(request).freeze
end

#sse?Boolean

Check if the request accepts SSE responses

Returns:

  • (Boolean)


81
82
83
# File 'lib/datastar/dispatcher.rb', line 81

def sse?
  !!(@request.get_header(HTTP_ACCEPT).to_s =~ SSE_ACCEPT_EXP)
end

#stream(streamer = nil, heartbeat: @heartbeat) {|sse| ... } ⇒ Object

Start a streaming response A generator object is passed to the block The generator supports all the Datastar methods listed above (it’s the same type) But you can call them multiple times to send multiple messages down an open SSE connection. This methods also captures exceptions raised in the block and triggers any error callbacks. Client disconnection errors trigger the @on_client_disconnect callbacks. Finally, when the block is done streaming, the @on_server_disconnect callbacks are triggered.

When multiple streams are scheduled this way, this SDK will spawn each block in separate threads (or fibers, depending on executor) and linearize their writes to the connection socket As a last step, the finalize callback is called with the view context and the response This is so that different frameworks can setup their responses correctly. 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.

Examples:


datastar.stream do |sse|
  total = 300
  sse.patch_elements(%(<progress data-signal-progress="0" id="progress" max="#{total}" data-attr-value="$progress">0</progress>))
  total.times do |i|
    sse.patch_signals(progress: i)
  end
end

datastar.stream do |sse|
  # update things here
end

datastar.stream do |sse|
  # more concurrent updates here
end

Disable heartbeat for a single response


datastar.stream(heartbeat: false) do |sse|
  sse.patch_elements(html)
end

Parameters:

  • streamer (#call(ServerSentEventGenerator), nil) (defaults to: nil)

    a callable to call with the generator

  • heartbeat (Numeric, false) (defaults to: @heartbeat)

    override the heartbeat interval for this call, or false to disable

Yield Parameters:

Returns:

  • (Object)

    depends on the finalize callback



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/datastar/dispatcher.rb', line 262

def stream(streamer = nil, heartbeat: @heartbeat, &block)
  heartbeat_was = @heartbeat
  @heartbeat = heartbeat

  streamer ||= block
  @streamers << streamer
  if @heartbeat && !@heartbeat_on
    @heartbeat_on = true
    @streamers << proc do |sse|
      while true
        sleep @heartbeat
        sse.check_connection!
      end
    end
  end

  body = if @streamers.size == 1
    stream_one(streamer) 
  else
    stream_many(streamer) 
  end

  @response.body = body
  @finalize.call(@view_context, @response)
ensure
  @heartbeat = heartbeat_was
end